View Javadoc
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 }