1 /*
2 Licensed to the Apache Software Foundation (ASF) under one or more
3 contributor license agreements. See the NOTICE file distributed with
4 this work for additional information regarding copyright ownership.
5 The ASF licenses this file to You under the Apache License, Version 2.0
6 (the "License"); you may not use this file except in compliance with
7 the License. You may obtain a copy of the License at
8
9 https://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 */
17 package org.apache.commons.cli.help;
18
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Comparator;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Objects;
26 import java.util.function.Function;
27 import java.util.function.Supplier;
28
29 import org.apache.commons.cli.Option;
30 import org.apache.commons.cli.OptionGroup;
31 import org.apache.commons.cli.Options;
32
33 /**
34 * Helps formatters provides the framework to link a {@link HelpAppendable} with a {@link OptionFormatter} and a default {@link TableDefinition} so to produce
35 * standardized format help output.
36 *
37 * @since 1.10.0
38 */
39 public abstract class AbstractHelpFormatter {
40
41 /**
42 * Abstracts building instances for subclasses.
43 * <ul>
44 * <li>helpAppendable = a {@link TextHelpAppendable} writing to {@code System.out}</li>
45 * <li>optionFormatter.Builder = the default {@link OptionFormatter.Builder}</li>
46 * </ul>
47 *
48 * @param <B> The builder type.
49 * @param <T> The type to build.
50 */
51 public abstract static class Builder<B extends Builder<B, T>, T extends AbstractHelpFormatter> implements Supplier<T> {
52
53 /** The comparator to sort lists of options */
54 private Comparator<Option> comparator = DEFAULT_COMPARATOR;
55
56 /** The {@link HelpAppendable}. */
57 private HelpAppendable helpAppendable = TextHelpAppendable.systemOut();
58
59 /** The {@link OptionFormatter.Builder} to use to format options in the table. */
60 private OptionFormatter.Builder optionFormatBuilder = OptionFormatter.builder();
61
62 /** The string to separate option groups. */
63 private String optionGroupSeparator = DEFAULT_OPTION_GROUP_SEPARATOR;
64
65 /**
66 * Constructs a new instance.
67 * <p>
68 * Sets {@code showSince} to {@code true}.
69 * </p>
70 */
71 protected Builder() {
72 // empty
73 }
74
75 /**
76 * Returns this instance cast to {@code B}.
77 *
78 * @return {@code this} instance cast to {@code B}.
79 */
80 @SuppressWarnings("unchecked")
81 protected B asThis() {
82 return (B) this;
83 }
84
85 /**
86 * Gets the comparator to sort lists of options.
87 *
88 * @return the comparator to sort lists of options.
89 */
90 protected Comparator<Option> getComparator() {
91 return comparator;
92 }
93
94 /**
95 * Gets {@link HelpAppendable}.
96 *
97 * @return the {@link HelpAppendable}.
98 */
99 protected HelpAppendable getHelpAppendable() {
100 return helpAppendable;
101 }
102
103 /**
104 * Gets {@link OptionFormatter.Builder} to use to format options in the table.
105 *
106 * @return the {@link OptionFormatter.Builder} to use to format options in the table.
107 */
108 protected OptionFormatter.Builder getOptionFormatBuilder() {
109 return optionFormatBuilder;
110 }
111
112 /**
113 * Gets string to separate option groups.
114 *
115 * @return the string to separate option groups.
116 */
117 protected String getOptionGroupSeparator() {
118 return optionGroupSeparator;
119 }
120
121 /**
122 * Sets the comparator to use for sorting options. If set to {@code null} no sorting is performed.
123 *
124 * @param comparator The comparator to use for sorting options.
125 * @return this
126 */
127 public B setComparator(final Comparator<Option> comparator) {
128 this.comparator = comparator;
129 return asThis();
130 }
131
132 /**
133 * Sets the {@link HelpAppendable}.
134 *
135 * @param helpAppendable the {@link HelpAppendable} to use.
136 * @return this
137 */
138 public B setHelpAppendable(final HelpAppendable helpAppendable) {
139 this.helpAppendable = helpAppendable != null ? helpAppendable : TextHelpAppendable.systemOut();
140 return asThis();
141 }
142
143 /**
144 * Sets the {@link OptionFormatter.Builder}.
145 *
146 * @param optionFormatBuilder the {@link OptionFormatter.Builder} to use.
147 * @return this
148 */
149 public B setOptionFormatBuilder(final OptionFormatter.Builder optionFormatBuilder) {
150 this.optionFormatBuilder = optionFormatBuilder != null ? optionFormatBuilder : OptionFormatter.builder();
151 return asThis();
152 }
153
154 /**
155 * Sets the OptionGroup separator. Normally " | " or something similar to denote that only one option may be chosen.
156 *
157 * @param optionGroupSeparator the string to separate option group elements with.
158 * @return this
159 */
160 public B setOptionGroupSeparator(final String optionGroupSeparator) {
161 this.optionGroupSeparator = Util.defaultValue(optionGroupSeparator, "");
162 return asThis();
163 }
164
165 }
166
167 /**
168 * The default comparator for {@link Option} implementations.
169 */
170 public static final Comparator<Option> DEFAULT_COMPARATOR = (opt1, opt2) -> opt1.getKey().compareToIgnoreCase(opt2.getKey());
171
172 /**
173 * The default separator between {@link OptionGroup} elements: {@value}.
174 */
175 public static final String DEFAULT_OPTION_GROUP_SEPARATOR = " | ";
176
177 /**
178 * The string to display at the beginning of the usage statement: {@value}.
179 */
180 public static final String DEFAULT_SYNTAX_PREFIX = "usage: ";
181
182 /** The comparator for sorting {@link Option} collections */
183 private final Comparator<Option> comparator;
184 /**
185 * The {@link HelpAppendable} that produces the final output.
186 */
187 private final HelpAppendable helpAppendable;
188
189 /**
190 * The OptionFormatter.Builder used to display options within the help page.
191 */
192 private final OptionFormatter.Builder optionFormatBuilder;
193
194 /** The separator between {@link OptionGroup} components. */
195 private final String optionGroupSeparator;
196
197 /**
198 * The phrase printed before the syntax line.
199 */
200 private String syntaxPrefix = DEFAULT_SYNTAX_PREFIX;
201
202 /**
203 * Constructs the base formatter.
204 *
205 * @param builder the builder.
206 */
207 protected AbstractHelpFormatter(final Builder<?, ?> builder) {
208 this.helpAppendable = Objects.requireNonNull(builder.getHelpAppendable(), "helpAppendable");
209 this.optionFormatBuilder = Objects.requireNonNull(builder.getOptionFormatBuilder(), "optionFormatBuilder");
210 this.comparator = Objects.requireNonNull(builder.getComparator(), "comparator");
211 this.optionGroupSeparator = Util.defaultValue(builder.getOptionGroupSeparator(), "");
212 }
213
214 /**
215 * Gets the comparator for sorting options.
216 *
217 * @return The comparator for sorting options.
218 */
219 protected Comparator<Option> getComparator() {
220 return comparator;
221 }
222
223 /**
224 * Gets the help appendable.
225 *
226 * @return The help appendable.
227 */
228 protected HelpAppendable getHelpAppendable() {
229 return helpAppendable;
230 }
231
232 /**
233 * Gets the option formatter builder.
234 *
235 * @return The option formatter builder.
236 */
237 protected OptionFormatter.Builder getOptionFormatBuilder() {
238 return optionFormatBuilder;
239 }
240
241 /**
242 * Constructs an {@link OptionFormatter} for the specified {@link Option}.
243 *
244 * @param option The Option to format.
245 * @return an {@link OptionFormatter} for the specified {@link Option}.
246 */
247 public final OptionFormatter getOptionFormatter(final Option option) {
248 return optionFormatBuilder.build(option);
249 }
250
251 /**
252 * Gets the option group separator.
253 *
254 * @return The option group separator.
255 */
256 protected String getOptionGroupSeparator() {
257 return optionGroupSeparator;
258 }
259
260 /**
261 * Gets the {@link HelpAppendable} associated with this help formatter.
262 *
263 * @return The {@link HelpAppendable} associated with this help formatter.
264 */
265 public final HelpAppendable getSerializer() {
266 return helpAppendable;
267 }
268
269 /**
270 * Gets the currently set syntax prefix.
271 *
272 * @return The currently set syntax prefix.
273 */
274 public final String getSyntaxPrefix() {
275 return syntaxPrefix;
276 }
277
278 /**
279 * Converts a collection of {@link Option}s into a {@link TableDefinition}.
280 *
281 * @param options The options to create a table for.
282 * @return the TableDefinition.
283 */
284 protected abstract TableDefinition getTableDefinition(Iterable<Option> options);
285
286 /**
287 * Prints the help for {@link Options} with the specified command line syntax.
288 *
289 * @param cmdLineSyntax the syntax for this application.
290 * @param header the banner to display at the beginning of the help.
291 * @param options the collection of {@link Option} objects to print.
292 * @param footer the banner to display at the end of the help.
293 * @param autoUsage whether to print an automatically generated usage statement.
294 * @throws IOException If the output could not be written to the {@link HelpAppendable}.
295 */
296 public void printHelp(final String cmdLineSyntax, final String header, final Iterable<Option> options, final String footer, final boolean autoUsage)
297 throws IOException {
298 Options optionsObject = new Options();
299 options.forEach(optionsObject::addOption);
300 printHelp(cmdLineSyntax, header, optionsObject, footer, autoUsage);
301 }
302
303 /**
304 * Prints the help for a collection of {@link Option}s with the specified command line syntax.
305 *
306 * @param cmdLineSyntax the syntax for this application.
307 * @param header the banner to display at the beginning of the help.
308 * @param options the collection of {@link Option} objects to print.
309 * @param footer the banner to display at the end of the help.
310 * @param autoUsage whether to print an automatically generated usage statement.
311 * @throws IOException If the output could not be written to the {@link HelpAppendable}.
312 */
313 public void printHelp(final String cmdLineSyntax, final String header, final Options options, final String footer, final boolean autoUsage)
314 throws IOException {
315 if (Util.isEmpty(cmdLineSyntax)) {
316 throw new IllegalArgumentException("cmdLineSyntax not provided");
317 }
318 if (autoUsage) {
319 helpAppendable.appendParagraphFormat("%s %s %s", syntaxPrefix, cmdLineSyntax, toSyntaxOptions(options));
320 } else {
321 helpAppendable.appendParagraphFormat("%s %s", syntaxPrefix, cmdLineSyntax);
322 }
323 if (!Util.isEmpty(header)) {
324 helpAppendable.appendParagraph(header);
325 }
326 helpAppendable.appendTable(getTableDefinition(options.getOptions()));
327 if (!Util.isEmpty(footer)) {
328 helpAppendable.appendParagraph(footer);
329 }
330 }
331
332 /**
333 * Prints the option table for a collection of {@link Option} objects to the {@link HelpAppendable}.
334 *
335 * @param options the collection of Option objects to print in the table.
336 * @throws IOException If the output could not be written to the {@link HelpAppendable}.
337 */
338 public final void printOptions(final Iterable<Option> options) throws IOException {
339 printOptions(getTableDefinition(options));
340 }
341
342 /**
343 * Prints the option table for the specified {@link Options} to the {@link HelpAppendable}.
344 *
345 * @param options the Options to print in the table.
346 * @throws IOException If the output could not be written to the {@link HelpAppendable}.
347 */
348 public final void printOptions(final Options options) throws IOException {
349 printOptions(options.getOptions());
350 }
351
352 /**
353 * Prints a {@link TableDefinition} to the {@link HelpAppendable}.
354 *
355 * @param tableDefinition the {@link TableDefinition} to print.
356 * @throws IOException If the output could not be written to the {@link HelpAppendable}.
357 */
358 public final void printOptions(final TableDefinition tableDefinition) throws IOException {
359 helpAppendable.appendTable(tableDefinition);
360 }
361
362 /**
363 * Sets the syntax prefix. This is the phrase that is printed before the syntax line.
364 *
365 * @param prefix the new value for the syntax prefix.
366 */
367 public final void setSyntaxPrefix(final String prefix) {
368 this.syntaxPrefix = prefix;
369 }
370
371 /**
372 * Creates a new list of options ordered by the comparator.
373 *
374 * @param options the Options to sort.
375 * @return a new list of options ordered by the comparator.
376 */
377 public List<Option> sort(final Iterable<Option> options) {
378 final List<Option> result = new ArrayList<>();
379 if (options != null) {
380 options.forEach(result::add);
381 result.sort(comparator);
382 }
383 return result;
384 }
385
386 /**
387 * Creates a new list of options ordered by the comparator.
388 *
389 * @param options the Options to sort.
390 * @return a new list of options ordered by the comparator.
391 */
392 public List<Option> sort(final Options options) {
393 return sort(options == null ? null : options.getOptions());
394 }
395
396 /**
397 * Formats the {@code argName} as an argument a defined in the enclosed {@link OptionFormatter.Builder}.
398 *
399 * @param argName the string to format as an argument.
400 * @return the {@code argName} formatted as an argument.
401 */
402 public final String toArgName(final String argName) {
403 return optionFormatBuilder.toArgName(argName);
404 }
405
406 /**
407 * Return the string representation of the options as used in the syntax display.
408 * <p>
409 * This is probably not the method you want. This method does not track the presence
410 * of option groups. To display the option grouping use {@link #toSyntaxOptions(Options)} or
411 * {@link #toSyntaxOptions(OptionGroup)} for individual groups.
412 * </p>
413 * @param options The collection of {@link Option} instances to create the string representation for.
414 * @return the string representation of the options as used in the syntax display.
415 */
416 public String toSyntaxOptions(final Iterable<Option> options) {
417 return toSyntaxOptions(options, o -> null);
418 }
419
420 /**
421 * Return the string representation of the options as used in the syntax display.
422 *
423 * @param options The options to create the string representation for.
424 * @param lookup a function to determine if the Option is part of an OptionGroup that has already been processed.
425 * @return the string representation of the options as used in the syntax display.
426 */
427 protected String toSyntaxOptions(final Iterable<Option> options, final Function<Option, OptionGroup> lookup) {
428 // list of groups that have been processed.
429 final Collection<OptionGroup> processedGroups = new ArrayList<>();
430 final List<Option> optList = sort(options);
431 final StringBuilder buff = new StringBuilder();
432 String prefix = "";
433 // iterate over the options
434 for (final Option option : optList) {
435 // get the next Option
436 // check if the option is part of an OptionGroup
437 final OptionGroup optionGroup = lookup.apply(option);
438 // if the option is part of a group
439 if (optionGroup != null) {
440 // and if the group has not already been processed
441 if (!processedGroups.contains(optionGroup)) {
442 // add the group to the processed list
443 processedGroups.add(optionGroup);
444 // add the usage clause
445 buff.append(prefix).append(toSyntaxOptions(optionGroup));
446 prefix = " ";
447 }
448 // otherwise the option was displayed in the group previously so ignore it.
449 }
450 // if the Option is not part of an OptionGroup
451 else {
452 buff.append(prefix).append(optionFormatBuilder.build(option).toSyntaxOption());
453 prefix = " ";
454 }
455 }
456 return buff.toString();
457 }
458
459 /**
460 * Return the string representation of the options as used in the syntax display.
461 *
462 * @param group The OptionGroup to create the string representation for.
463 * @return the string representation of the options as used in the syntax display.
464 */
465 public String toSyntaxOptions(final OptionGroup group) {
466 final StringBuilder buff = new StringBuilder();
467 final List<Option> optList = sort(group.getOptions());
468 OptionFormatter formatter = null;
469 // for each option in the OptionGroup
470 final Iterator<Option> iter = optList.iterator();
471 while (iter.hasNext()) {
472 formatter = optionFormatBuilder.build(iter.next());
473 // whether the option is required or not is handled at group level
474 buff.append(formatter.toSyntaxOption(true));
475 if (iter.hasNext()) {
476 buff.append(optionGroupSeparator);
477 }
478 }
479 if (formatter != null) {
480 return group.isRequired() ? buff.toString() : formatter.toOptional(buff.toString());
481 }
482 return ""; // there were no entries in the group.
483 }
484
485 /**
486 * Return the string representation of the options as used in the syntax display.
487 *
488 * @param options The {@link Options} to create the string representation for.
489 * @return the string representation of the options as used in the syntax display.
490 */
491 public String toSyntaxOptions(final Options options) {
492 return toSyntaxOptions(options.getOptions(), options::getOptionGroup);
493 }
494 }