    001/*
    002  Licensed to the Apache Software Foundation (ASF) under one or more
    003  contributor license agreements.  See the NOTICE file distributed with
    004  this work for additional information regarding copyright ownership.
    005  The ASF licenses this file to You under the Apache License, Version 2.0
    006  (the "License"); you may not use this file except in compliance with
    007  the License.  You may obtain a copy of the License at
    008
    009      https://www.apache.org/licenses/LICENSE-2.0
    010
    011  Unless required by applicable law or agreed to in writing, software
    012  distributed under the License is distributed on an "AS IS" BASIS,
    013  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014  See the License for the specific language governing permissions and
    015  limitations under the License.
    016 */
    017package org.apache.commons.cli.help;
    018
    019import java.io.IOException;
    020import java.util.ArrayList;
    021import java.util.Collection;
    022import java.util.Comparator;
    023import java.util.Iterator;
    024import java.util.List;
    025import java.util.Objects;
    026import java.util.function.Function;
    027import java.util.function.Supplier;
    028
    029import org.apache.commons.cli.Option;
    030import org.apache.commons.cli.OptionGroup;
    031import org.apache.commons.cli.Options;
    032
    033/**
    034 * Helps formatters provides the framework to link a {@link HelpAppendable} with a {@link OptionFormatter} and a default {@link TableDefinition} so to produce
    035 * standardized format help output.
    036 *
    037 * @since 1.10.0
    038 */
    039public abstract class AbstractHelpFormatter {
    040
    041    /**
    042     * Abstracts building instances for subclasses.
    043     * <ul>
    044     * <li>helpAppendable = a {@link TextHelpAppendable} writing to {@code System.out}</li>
    045     * <li>optionFormatter.Builder = the default {@link OptionFormatter.Builder}</li>
    046     * </ul>
    047     *
    048     * @param <B> The builder type.
    049     * @param <T> The type to build.
    050     */
    051    public abstract static class Builder<B extends Builder<B, T>, T extends AbstractHelpFormatter> implements Supplier<T> {
    052
    053        /** The comparator to sort lists of options */
    054        private Comparator<Option> comparator = DEFAULT_COMPARATOR;
    055
    056        /** The {@link HelpAppendable}. */
    057        private HelpAppendable helpAppendable = TextHelpAppendable.systemOut();
    058
    059        /** The {@link OptionFormatter.Builder} to use to format options in the table. */
    060        private OptionFormatter.Builder optionFormatBuilder = OptionFormatter.builder();
    061
    062        /** The string to separate option groups. */
    063        private String optionGroupSeparator = DEFAULT_OPTION_GROUP_SEPARATOR;
    064
    065        /**
    066         * Constructs a new instance.
    067         * <p>
    068         * Sets {@code showSince} to {@code true}.
    069         * </p>
    070         */
    071        protected Builder() {
    072            // empty
    073        }
    074
    075        /**
    076         * Returns this instance cast to {@code B}.
    077         *
    078         * @return this instance cast to {@code B}.
    079         */
    080        @SuppressWarnings("unchecked")
    081        protected B asThis() {
    082            return (B) this;
    083        }
    084
    085        /**
    086         * Gets the comparator to sort lists of options.
    087         *
    088         * @return the comparator to sort lists of options.
    089         */
    090        protected Comparator<Option> getComparator() {
    091            return comparator;
    092        }
    093
    094        /**
    095         * Gets {@link HelpAppendable}.
    096         *
    097         * @return the {@link HelpAppendable}.
    098         */
    099        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 a collection of {@link Option}s 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        if (Util.isEmpty(cmdLineSyntax)) {
    299            throw new IllegalArgumentException("cmdLineSyntax not provided");
    300        }
    301        if (autoUsage) {
    302            helpAppendable.appendParagraphFormat("%s %s %s", syntaxPrefix, cmdLineSyntax, toSyntaxOptions(options));
    303        } else {
    304            helpAppendable.appendParagraphFormat("%s %s", syntaxPrefix, cmdLineSyntax);
    305        }
    306        if (!Util.isEmpty(header)) {
    307            helpAppendable.appendParagraph(header);
    308        }
    309        helpAppendable.appendTable(getTableDefinition(options));
    310        if (!Util.isEmpty(footer)) {
    311            helpAppendable.appendParagraph(footer);
    312        }
    313    }
    314
    315    /**
    316     * Prints the help for {@link Options} with the specified command line syntax.
    317     *
    318     * @param cmdLineSyntax the syntax for this application
    319     * @param header        the banner to display at the beginning of the help
    320     * @param options       the {@link Options} to print
    321     * @param footer        the banner to display at the end of the help
    322     * @param autoUsage     whether to print an automatically generated usage statement
    323     * @throws IOException If the output could not be written to the {@link HelpAppendable}
    324     */
    325    public final void printHelp(final String cmdLineSyntax, final String header, final Options options, final String footer, final boolean autoUsage)
    326            throws IOException {
    327        printHelp(cmdLineSyntax, header, options.getOptions(), footer, autoUsage);
    328    }
    329
    330    /**
    331     * Prints the option table for a collection of {@link Option} objects to the {@link HelpAppendable}.
    332     *
    333     * @param options the collection of Option objects to print in the table.
    334     * @throws IOException If the output could not be written to the {@link HelpAppendable}
    335     */
    336    public final void printOptions(final Iterable<Option> options) throws IOException {
    337        printOptions(getTableDefinition(options));
    338    }
    339
    340    /**
    341     * Prints the option table for the specified {@link Options} to the {@link HelpAppendable}.
    342     *
    343     * @param options the Options to print in the table.
    344     * @throws IOException If the output could not be written to the {@link HelpAppendable}
    345     */
    346    public final void printOptions(final Options options) throws IOException {
    347        printOptions(options.getOptions());
    348    }
    349
    350    /**
    351     * Prints a {@link TableDefinition} to the {@link HelpAppendable}.
    352     *
    353     * @param tableDefinition the {@link TableDefinition} to print.
    354     * @throws IOException If the output could not be written to the {@link HelpAppendable}
    355     */
    356    public final void printOptions(final TableDefinition tableDefinition) throws IOException {
    357        helpAppendable.appendTable(tableDefinition);
    358    }
    359
    360    /**
    361     * Sets the syntax prefix. This is the phrase that is printed before the syntax line.
    362     *
    363     * @param prefix the new value for the syntax prefix.
    364     */
    365    public final void setSyntaxPrefix(final String prefix) {
    366        this.syntaxPrefix = prefix;
    367    }
    368
    369    /**
    370     * Creates a new list of options ordered by the comparator.
    371     *
    372     * @param options the Options to sort.
    373     * @return a new list of options ordered by the comparator.
    374     */
    375    public List<Option> sort(final Iterable<Option> options) {
    376        final List<Option> result = new ArrayList<>();
    377        if (options != null) {
    378            options.forEach(result::add);
    379            result.sort(comparator);
    380        }
    381        return result;
    382    }
    383
    384    /**
    385     * Creates a new list of options ordered by the comparator.
    386     *
    387     * @param options the Options to sort.
    388     * @return a new list of options ordered by the comparator.
    389     */
    390    public List<Option> sort(final Options options) {
    391        return sort(options == null ? null : options.getOptions());
    392    }
    393
    394    /**
    395     * Formats the {@code argName} as an argument a defined in the enclosed {@link OptionFormatter.Builder}
    396     *
    397     * @param argName the string to format as an argument.
    398     * @return the {@code argName} formatted as an argument.
    399     */
    400    public final String toArgName(final String argName) {
    401        return optionFormatBuilder.toArgName(argName);
    402    }
    403
    404    /**
    405     * Return the string representation of the options as used in the syntax display.
    406     *
    407     * @param options The collection of {@link Option} instances to create the string representation for.
    408     * @return the string representation of the options as used in the syntax display.
    409     */
    410    public String toSyntaxOptions(final Iterable<Option> options) {
    411        return toSyntaxOptions(options, o -> null);
    412    }
    413
    414    /**
    415     * Return the string representation of the options as used in the syntax display.
    416     *
    417     * @param options The options to create the string representation for.
    418     * @param lookup  a function to determine if the Option is part of an OptionGroup that has already been processed.
    419     * @return the string representation of the options as used in the syntax display.
    420     */
    421    protected String toSyntaxOptions(final Iterable<Option> options, final Function<Option, OptionGroup> lookup) {
    422        // list of groups that have been processed.
    423        final Collection<OptionGroup> processedGroups = new ArrayList<>();
    424        final List<Option> optList = sort(options);
    425        final StringBuilder buff = new StringBuilder();
    426        String prefix = "";
    427        // iterate over the options
    428        for (final Option option : optList) {
    429            // get the next Option
    430            // check if the option is part of an OptionGroup
    431            final OptionGroup optionGroup = lookup.apply(option);
    432            // if the option is part of a group
    433            if (optionGroup != null) {
    434                // and if the group has not already been processed
    435                if (!processedGroups.contains(optionGroup)) {
    436                    // add the group to the processed list
    437                    processedGroups.add(optionGroup);
    438                    // add the usage clause
    439                    buff.append(prefix).append(toSyntaxOptions(optionGroup));
    440                    prefix = " ";
    441                }
    442                // otherwise the option was displayed in the group previously so ignore it.
    443            }
    444            // if the Option is not part of an OptionGroup
    445            else {
    446                buff.append(prefix).append(optionFormatBuilder.build(option).toSyntaxOption());
    447                prefix = " ";
    448            }
    449        }
    450        return buff.toString();
    451    }
    452
    453    /**
    454     * Return the string representation of the options as used in the syntax display.
    455     *
    456     * @param group The OptionGroup to create the string representation for.
    457     * @return the string representation of the options as used in the syntax display.
    458     */
    459    public String toSyntaxOptions(final OptionGroup group) {
    460        final StringBuilder buff = new StringBuilder();
    461        final List<Option> optList = sort(group.getOptions());
    462        OptionFormatter formatter = null;
    463        // for each option in the OptionGroup
    464        final Iterator<Option> iter = optList.iterator();
    465        while (iter.hasNext()) {
    466            formatter = optionFormatBuilder.build(iter.next());
    467            // whether the option is required or not is handled at group level
    468            buff.append(formatter.toSyntaxOption(true));
    469
    470            if (iter.hasNext()) {
    471                buff.append(optionGroupSeparator);
    472            }
    473        }
    474        if (formatter != null) {
    475            return group.isRequired() ? buff.toString() : formatter.toOptional(buff.toString());
    476        }
    477        return ""; // there were no entries in the group.
    478    }
    479
    480    /**
    481     * Return the string representation of the options as used in the syntax display.
    482     *
    483     * @param options The {@link Options} to create the string representation for.
    484     * @return the string representation of the options as used in the syntax display.
    485     */
    486    public String toSyntaxOptions(final Options options) {
    487        return toSyntaxOptions(options.getOptions(), options::getOptionGroup);
    488    }
    489}