    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 */
    017
    018package org.apache.commons.cli;
    019
    020import java.util.ArrayList;
    021import java.util.Enumeration;
    022import java.util.List;
    023import java.util.Objects;
    024import java.util.Properties;
    025import java.util.function.Consumer;
    026import java.util.function.Supplier;
    027
    028import org.apache.commons.cli.help.OptionFormatter;
    029
    030/**
    031 * Default parser.
    032 *
    033 * @since 1.3
    034 */
    035public class DefaultParser implements CommandLineParser {
    036
    037    /**
    038     * A nested builder class to create {@code DefaultParser} instances
    039     * using descriptive methods.
    040     *
    041     * Example usage:
    042     * <pre>
    043     * DefaultParser parser = Option.builder()
    044     *     .setAllowPartialMatching(false)
    045     *     .setStripLeadingAndTrailingQuotes(false)
    046     *     .build();
    047     * </pre>
    048     *
    049     * @since 1.5.0
    050     */
    051    public static final class Builder implements Supplier<DefaultParser> {
    052
    053        /** Flag indicating if partial matching of long options is supported. */
    054        private boolean allowPartialMatching = true;
    055
    056        /**
    057         * The deprecated option handler.
    058         * <p>
    059         * If you want to serialize this field, use a serialization proxy.
    060         * </p>
    061         */
    062        private Consumer<Option> deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
    063
    064        /** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments. */
    065        private Boolean stripLeadingAndTrailingQuotes;
    066
    067        /**
    068         * Constructs a new {@code Builder} for a {@code DefaultParser} instance.
    069         * <p>
    070         * Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by default, mimicking the argument-less constructor.
    071         * </p>
    072         */
    073        private Builder() {
    074        }
    075
    076        /**
    077         * Builds an DefaultParser with the values declared by this {@link Builder}.
    078         *
    079         * @return the new {@link DefaultParser}.
    080         * @since 1.5.0
    081         * @deprecated Use {@link #get()}.
    082         */
    083        @Deprecated
    084        public DefaultParser build() {
    085            return get();
    086        }
    087
    088        /**
    089         * Builds an DefaultParser with the values declared by this {@link Builder}.
    090         *
    091         * @return the new {@link DefaultParser}.
    092         * @since 1.10.0
    093         */
    094        @Override
    095        public DefaultParser get() {
    096            return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes, deprecatedHandler);
    097        }
    098
    099        /**
    100         * Sets if partial matching of long options is supported.
    101         * <p>
    102         * By "partial matching" we mean that given the following code:
    103         * </p>
    104         *
    105         * <pre>{@code
    106         * final Options options = new Options();
    107         * options.addOption(new Option("d", "debug", false, "Turn on debug."));
    108         * options.addOption(new Option("e", "extract", false, "Turn on extract."));
    109         * options.addOption(new Option("o", "option", true, "Turn on option with argument."));
    110         * }</pre>
    111         * <p>
    112         * If "partial matching" is turned on, {@code -de} only matches the {@code "debug"} option. However, with "partial matching" disabled, {@code -de} would
    113         * enable both {@code debug} as well as {@code extract}
    114         * </p>
    115         *
    116         * @param allowPartialMatching whether to allow partial matching of long options.
    117         * @return {@code this} instance.
    118         * @since 1.5.0
    119         */
    120        public Builder setAllowPartialMatching(final boolean allowPartialMatching) {
    121            this.allowPartialMatching = allowPartialMatching;
    122            return this;
    123        }
    124
    125        /**
    126         * Sets the deprecated option handler.
    127         *
    128         * @param deprecatedHandler the deprecated option handler.
    129         * @return {@code this} instance.
    130         * @since 1.7.0
    131         */
    132        public Builder setDeprecatedHandler(final Consumer<Option> deprecatedHandler) {
    133            this.deprecatedHandler = deprecatedHandler;
    134            return this;
    135        }
    136
    137        /**
    138         * Sets if balanced leading and trailing double quotes should be stripped from option arguments.
    139         *
    140         * <p>
    141         * If "stripping of balanced leading and trailing double quotes from option arguments" is true, the outermost balanced double quotes of option arguments
    142         * values will be removed. For example, {@code -o '"x"'} getValue() will return {@code x}, instead of {@code "x"}
    143         * </p>
    144         * <p>
    145         * If "stripping of balanced leading and trailing double quotes from option arguments" is null, then quotes will be stripped from option values
    146         * separated by space from the option, but kept in other cases, which is the historic behavior.
    147         * </p>
    148         *
    149         * @param stripLeadingAndTrailingQuotes whether balanced leading and trailing double quotes should be stripped from option arguments.
    150         * @return {@code this} instance.
    151         * @since 1.5.0
    152         */
    153        public Builder setStripLeadingAndTrailingQuotes(final Boolean stripLeadingAndTrailingQuotes) {
    154            this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
    155            return this;
    156        }
    157    }
    158
    159    /**
    160     * Enum representing possible actions that may be done when "non option" is discovered during parsing.
    161     *
    162     * @since 1.10.0
    163     */
    164    public enum NonOptionAction {
    165        /**
    166         * Parsing continues and current token is ignored.
    167         */
    168        IGNORE,
    169        /**
    170         * Parsing continues and current token is added to command line arguments.
    171         */
    172        SKIP,
    173        /**
    174         * Parsing will stop and remaining tokens are added to command line arguments.
    175         * Equivalent of {@code stopAtNonOption = true}.
    176         */
    177        STOP,
    178        /**
    179         * Parsing will abort and exception is thrown.
    180         * Equivalent of {@code stopAtNonOption = false}.
    181         */
    182        THROW;
    183    }
    184
    185    /**
    186     * Creates a new {@link Builder} to create an {@link DefaultParser} using descriptive
    187     * methods.
    188     *
    189     * @return a new {@link Builder} instance
    190     * @since 1.5.0
    191     */
    192    public static Builder builder() {
    193        return new Builder();
    194    }
    195
    196    static int indexOfEqual(final String token) {
    197        return token.indexOf(Char.EQUAL);
    198    }
    199
    200    /** The command-line instance. */
    201    protected CommandLine cmd;
    202
    203    /** The current options. */
    204    protected Options options;
    205
    206    /**
    207     * Flag indicating how unrecognized tokens are handled. {@code true} to stop the parsing and add the remaining
    208     * tokens to the args list. {@code false} to throw an exception.
    209     *
    210     * @deprecated Use {@link #nonOptionAction} instead. This field is unused, and left for binary compatibility reasons.
    211     */
    212    @Deprecated
    213    protected boolean stopAtNonOption;
    214
    215    /**
    216     * Action to happen when "non option" token is discovered.
    217     *
    218     * @since 1.10.0
    219     */
    220    protected NonOptionAction nonOptionAction;
    221
    222    /** The token currently processed. */
    223    protected String currentToken;
    224
    225    /** The last option parsed. */
    226    protected Option currentOption;
    227
    228    /** Flag indicating if tokens should no longer be analyzed and simply added as arguments of the command line. */
    229    protected boolean skipParsing;
    230
    231    /** The required options and groups expected to be found when parsing the command line. */
    232    // This can contain either a String (addOption) or an OptionGroup (addOptionGroup)
    233    // TODO this seems wrong
    234    protected List expectedOpts;
    235
    236    /** Flag indicating if partial matching of long options is supported. */
    237    private final boolean allowPartialMatching;
    238
    239    /** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments.
    240     * null represents the historic arbitrary behavior */
    241    private final Boolean stripLeadingAndTrailingQuotes;
    242
    243    /**
    244     * The deprecated option handler.
    245     * <p>
    246     * If you want to serialize this field, use a serialization proxy.
    247     * </p>
    248     */
    249    private final Consumer<Option> deprecatedHandler;
    250
    251    /**
    252     * Creates a new DefaultParser instance with partial matching enabled.
    253     * <p>
    254     * By "partial matching" we mean that given the following code:
    255     * </p>
    256     *
    257     * <pre>{@code
    258     * final Options options = new Options();
    259     * options.addOption(new Option("d", "debug", false, "Turn on debug."));
    260     * options.addOption(new Option("e", "extract", false, "Turn on extract."));
    261     * options.addOption(new Option("o", "option", true, "Turn on option with argument."));
    262     * }
    263     * </pre>
    264     *
    265     * with "partial matching" turned on, {@code -de} only matches the {@code "debug"} option. However, with
    266     * "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
    267     * options.
    268     */
    269    public DefaultParser() {
    270        this.allowPartialMatching = true;
    271        this.stripLeadingAndTrailingQuotes = null;
    272        this.deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
    273    }
    274
    275    /**
    276     * Create a new DefaultParser instance with the specified partial matching policy.
    277     * <p>
    278     * By "partial matching" we mean that given the following code:
    279     * </p>
    280     * <pre>{@code
    281     *     final Options options = new Options();
    282     *     options.addOption(new Option("d", "debug", false, "Turn on debug."));
    283     *     options.addOption(new Option("e", "extract", false, "Turn on extract."));
    284     *     options.addOption(new Option("o", "option", true, "Turn on option with argument."));
    285     * }
    286     * </pre>
    287     * <p>
    288     * with "partial matching" turned on, {@code -de} only matches the {@code "debug"} option. However, with
    289     * "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
    290     * options.
    291     * </p>
    292     *
    293     * @param allowPartialMatching if partial matching of long options shall be enabled
    294     */
    295    public DefaultParser(final boolean allowPartialMatching) {
    296        this.allowPartialMatching = allowPartialMatching;
    297        this.stripLeadingAndTrailingQuotes = null;
    298        this.deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
    299    }
    300
    301    /**
    302     * Creates a new DefaultParser instance with the specified partial matching and quote
    303     * stripping policy.
    304     *
    305     * @param allowPartialMatching if partial matching of long options shall be enabled
    306     * @param stripLeadingAndTrailingQuotes if balanced outer double quoutes should be stripped
    307     */
    308    private DefaultParser(final boolean allowPartialMatching, final Boolean stripLeadingAndTrailingQuotes, final Consumer<Option> deprecatedHandler) {
    309        this.allowPartialMatching = allowPartialMatching;
    310        this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
    311        this.deprecatedHandler = deprecatedHandler;
    312    }
    313
    314    /**
    315     * Adds token to command line {@link CommandLine#addArg(String)}.
    316     *
    317     * @param token the unrecognized option/argument.
    318     * @since 1.10.0
    319     */
    320    protected void addArg(final String token) {
    321        cmd.addArg(token);
    322    }
    323
    324    /**
    325     * Throws a {@link MissingArgumentException} if the current option didn't receive the number of arguments expected.
    326     */
    327    private void checkRequiredArgs() throws ParseException {
    328        if (currentOption != null && currentOption.requiresArg()) {
    329            if (isJavaProperty(currentOption.getKey()) && currentOption.getValuesList().size() == 1) {
    330                return;
    331            }
    332            throw new MissingArgumentException(currentOption);
    333        }
    334    }
    335
    336    /**
    337     * Throws a {@link MissingOptionException} if all of the required options are not present.
    338     *
    339     * @throws MissingOptionException if any of the required Options are not present.
    340     */
    341    protected void checkRequiredOptions() throws MissingOptionException {
    342        // if there are required options that have not been processed
    343        if (!expectedOpts.isEmpty()) {
    344            throw new MissingOptionException(expectedOpts);
    345        }
    346    }
    347
    348    /**
    349     * Searches for a prefix that is the long name of an option (-Xmx512m).
    350     *
    351     * @param token
    352     */
    353    private String getLongPrefix(final String token) {
    354        final String t = Util.stripLeadingHyphens(token);
    355        int i;
    356        String opt = null;
    357        for (i = t.length() - 2; i > 1; i--) {
    358            final String prefix = t.substring(0, i);
    359            if (options.hasLongOption(prefix)) {
    360                opt = prefix;
    361                break;
    362            }
    363        }
    364        return opt;
    365    }
    366
    367    /**
    368     * Gets a list of matching option strings for the given token, depending on the selected partial matching policy.
    369     *
    370     * @param token the token (may contain leading dashes).
    371     * @return the list of matching option strings or an empty list if no matching option could be found.
    372     */
    373    private List<String> getMatchingLongOptions(final String token) {
    374        if (allowPartialMatching) {
    375            return options.getMatchingOptions(token);
    376        }
    377        final List<String> matches = new ArrayList<>(1);
    378        if (options.hasLongOption(token)) {
    379            matches.add(options.getOption(token).getLongOpt());
    380        }
    381        return matches;
    382    }
    383
    384    /**
    385     * Breaks {@code token} into its constituent parts using the following algorithm.
    386     *
    387     * <ul>
    388     * <li>ignore the first character ("<strong>-</strong>")</li>
    389     * <li>for each remaining character check if an {@link Option} exists with that id.</li>
    390     * <li>if an {@link Option} does exist then add that character prepended with "<strong>-</strong>" to the list of processed
    391     * tokens.</li>
    392     * <li>if the {@link Option} can have an argument value and there are remaining characters in the token then add the
    393     * remaining characters as a token to the list of processed tokens.</li>
    394     * <li>if an {@link Option} does <strong>NOT</strong> exist <strong>AND</strong> {@code stopAtNonOption} <strong>IS</strong> set then add the
    395     * special token "<strong>--</strong>" followed by the remaining characters and also the remaining tokens directly to the
    396     * processed tokens list.</li>
    397     * <li>if an {@link Option} does <strong>NOT</strong> exist <strong>AND</strong> {@code stopAtNonOption} <strong>IS NOT</strong> set then add
    398     * that character prepended with "<strong>-</strong>".</li>
    399     * </ul>
    400     *
    401     * @param token The current token to be <strong>burst</strong> at the first non-Option encountered.
    402     * @throws ParseException if there are any problems encountered while parsing the command line token.
    403     */
    404    protected void handleConcatenatedOptions(final String token) throws ParseException {
    405        for (int i = 1; i < token.length(); i++) {
    406            final String ch = String.valueOf(token.charAt(i));
    407            if (!options.hasOption(ch)) {
    408                handleUnknownToken(nonOptionAction == NonOptionAction.STOP && i > 1 ? token.substring(i) : token);
    409                break;
    410            }
    411            handleOption(options.getOption(ch));
    412            if (currentOption != null && token.length() != i + 1) {
    413                // add the trail as an argument of the option
    414                currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(i + 1)));
    415                break;
    416            }
    417        }
    418    }
    419
    420    /**
    421     * Handles the following tokens:
    422     * <pre>
    423     * --L --L=V --L V --l
    424     * </pre>
    425     *
    426     * @param token the command line token to handle.
    427     */
    428    private void handleLongOption(final String token) throws ParseException {
    429        if (indexOfEqual(token) == -1) {
    430            handleLongOptionWithoutEqual(token);
    431        } else {
    432            handleLongOptionWithEqual(token);
    433        }
    434    }
    435
    436    /**
    437     * Handles the following tokens:
    438     * <pre>
    439     * --L=V -L=V --l=V -l=V
    440     * </pre>
    441     *
    442     * @param token the command line token to handle.
    443     */
    444    private void handleLongOptionWithEqual(final String token) throws ParseException {
    445        final int pos = indexOfEqual(token);
    446        final String value = token.substring(pos + 1);
    447        final String opt = token.substring(0, pos);
    448        final List<String> matchingOpts = getMatchingLongOptions(opt);
    449        if (matchingOpts.isEmpty()) {
    450            handleUnknownToken(currentToken);
    451        } else if (matchingOpts.size() > 1 && !options.hasLongOption(opt)) {
    452            throw new AmbiguousOptionException(opt, matchingOpts);
    453        } else {
    454            final String key = options.hasLongOption(opt) ? opt : matchingOpts.get(0);
    455            final Option option = options.getOption(key);
    456            if (option.acceptsArg()) {
    457                handleOption(option);
    458                currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(value));
    459                currentOption = null;
    460            } else {
    461                handleUnknownToken(currentToken);
    462            }
    463        }
    464    }
    465
    466    /**
    467     * Handles the following tokens:
    468     *
    469     * <pre>
    470     * --L -L --l -l
    471     * </pre>
    472     *
    473     * @param token the command line token to handle.
    474     */
    475    private void handleLongOptionWithoutEqual(final String token) throws ParseException {
    476        final List<String> matchingOpts = getMatchingLongOptions(token);
    477        if (matchingOpts.isEmpty()) {
    478            handleUnknownToken(currentToken);
    479        } else if (matchingOpts.size() > 1 && !options.hasLongOption(token)) {
    480            throw new AmbiguousOptionException(token, matchingOpts);
    481        } else {
    482            final String key = options.hasLongOption(token) ? token : matchingOpts.get(0);
    483            handleOption(options.getOption(key));
    484        }
    485    }
    486
    487    private void handleOption(final Option option) throws ParseException {
    488        // check the previous option before handling the next one
    489        checkRequiredArgs();
    490        final Option copy = (Option) option.clone();
    491        updateRequiredOptions(copy);
    492        cmd.addOption(copy);
    493        currentOption = copy.hasArg() ? copy : null;
    494    }
    495
    496    /**
    497     * Sets the values of Options using the values in {@code properties}.
    498     *
    499     * @param properties The value properties to be processed.
    500     */
    501    private void handleProperties(final Properties properties) throws ParseException {
    502        if (properties == null) {
    503            return;
    504        }
    505        for (final Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
    506            final String option = e.nextElement().toString();
    507            final Option opt = options.getOption(option);
    508            if (opt == null) {
    509                throw new UnrecognizedOptionException("Default option wasn't defined", option);
    510            }
    511            // if the option is part of a group, check if another option of the group has been selected
    512            final OptionGroup optionGroup = options.getOptionGroup(opt);
    513            final boolean selected = optionGroup != null && optionGroup.isSelected();
    514            if (!cmd.hasOption(option) && !selected) {
    515                // get the value from the properties
    516                final String value = properties.getProperty(option);
    517
    518                if (opt.hasArg()) {
    519                    if (opt.isValuesEmpty()) {
    520                        opt.processValue(stripLeadingAndTrailingQuotesDefaultOff(value));
    521                    }
    522                } else if (!("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value))) {
    523                    // if the value is not yes, true or 1 then don't add the option to the CommandLine
    524                    continue;
    525                }
    526                handleOption(opt);
    527                currentOption = null;
    528            }
    529        }
    530    }
    531
    532    /**
    533     * Handles the following tokens:
    534     * <pre>
    535     * -S -SV -S V -S=V -S1S2 -S1S2 V -SV1=V2
    536     *
    537     * -L -LV -L V -L=V -l
    538     * </pre>
    539     *
    540     * @param hyphenToken the command line token to handle.
    541     */
    542    private void handleShortAndLongOption(final String hyphenToken) throws ParseException {
    543        final String token = Util.stripLeadingHyphens(hyphenToken);
    544        final int pos = indexOfEqual(token);
    545        if (token.length() == 1) {
    546            // -S
    547            if (options.hasShortOption(token)) {
    548                handleOption(options.getOption(token));
    549            } else {
    550                handleUnknownToken(hyphenToken);
    551            }
    552        } else if (pos == -1) {
    553            // no equal sign found (-xxx)
    554            if (options.hasShortOption(token)) {
    555                handleOption(options.getOption(token));
    556            } else if (!getMatchingLongOptions(token).isEmpty()) {
    557                // -L or -l
    558                handleLongOptionWithoutEqual(hyphenToken);
    559            } else {
    560                // look for a long prefix (-Xmx512m)
    561                final String opt = getLongPrefix(token);
    562
    563                if (opt != null && options.getOption(opt).acceptsArg()) {
    564                    handleOption(options.getOption(opt));
    565                    currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(opt.length())));
    566                    currentOption = null;
    567                } else if (isJavaProperty(token)) {
    568                    // -SV1 (-Dflag)
    569                    handleOption(options.getOption(token.substring(0, 1)));
    570                    currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(1)));
    571                    currentOption = null;
    572                } else {
    573                    // -S1S2S3 or -S1S2V
    574                    handleConcatenatedOptions(hyphenToken);
    575                }
    576            }
    577        } else {
    578            // equal sign found (-xxx=yyy)
    579            final String opt = token.substring(0, pos);
    580            final String value = token.substring(pos + 1);
    581
    582            if (opt.length() == 1) {
    583                // -S=V
    584                final Option option = options.getOption(opt);
    585                if (option != null && option.acceptsArg()) {
    586                    handleOption(option);
    587                    currentOption.processValue(value);
    588                    currentOption = null;
    589                } else {
    590                    handleUnknownToken(hyphenToken);
    591                }
    592            } else if (isJavaProperty(opt)) {
    593                // -SV1=V2 (-Dkey=value)
    594                handleOption(options.getOption(opt.substring(0, 1)));
    595                currentOption.processValue(opt.substring(1));
    596                currentOption.processValue(value);
    597                currentOption = null;
    598            } else {
    599                // -L=V or -l=V
    600                handleLongOptionWithEqual(hyphenToken);
    601            }
    602        }
    603    }
    604
    605    /**
    606     * Handles any command line token.
    607     *
    608     * @param token the command line token to handle.
    609     * @throws ParseException
    610     */
    611    private void handleToken(final String token) throws ParseException {
    612        if (token != null) {
    613            currentToken = token;
    614            if (skipParsing) {
    615                addArg(token);
    616            } else if (OptionFormatter.DEFAULT_LONG_OPT_PREFIX.equals(token)) {
    617                skipParsing = true;
    618            } else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
    619                currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOn(token));
    620            } else if (token.startsWith(OptionFormatter.DEFAULT_LONG_OPT_PREFIX)) {
    621                handleLongOption(token);
    622            } else if (token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) && !OptionFormatter.DEFAULT_OPT_PREFIX.equals(token)) {
    623                handleShortAndLongOption(token);
    624            } else {
    625                handleUnknownToken(token);
    626            }
    627            if (currentOption != null && !currentOption.acceptsArg()) {
    628                currentOption = null;
    629            }
    630        }
    631    }
    632
    633    /**
    634     * Handles an unknown token. If the token starts with a dash an UnrecognizedOptionException is thrown. Otherwise the
    635     * token is added to the arguments of the command line. If the stopAtNonOption flag is set, this stops the parsing and
    636     * the remaining tokens are added as-is in the arguments of the command line.
    637     *
    638     * @param token the command line token to handle.
    639     * @throws ParseException if parsing should fail.
    640     * @since 1.10.0
    641     */
    642    protected void handleUnknownToken(final String token) throws ParseException {
    643        if (token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) && token.length() > 1 && nonOptionAction == NonOptionAction.THROW) {
    644            throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
    645        }
    646        if (!token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) || token.equals(OptionFormatter.DEFAULT_OPT_PREFIX)
    647                || token.length() > 1 && nonOptionAction != NonOptionAction.IGNORE) {
    648            addArg(token);
    649        }
    650        if (nonOptionAction == NonOptionAction.STOP) {
    651            skipParsing = true;
    652        }
    653    }
    654
    655    /**
    656     * Tests if the token is a valid argument.
    657     *
    658     * @param token
    659     */
    660    private boolean isArgument(final String token) {
    661        return !isOption(token) || isNegativeNumber(token);
    662    }
    663
    664    /**
    665     * Tests if the specified token is a Java-like property (-Dkey=value).
    666     */
    667    private boolean isJavaProperty(final String token) {
    668        final String opt = token.isEmpty() ? null : token.substring(0, 1);
    669        final Option option = options.getOption(opt);
    670        return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
    671    }
    672
    673    /**
    674     * Tests if the token looks like a long option.
    675     *
    676     * @param token
    677     */
    678    private boolean isLongOption(final String token) {
    679        if (token == null || !token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) || token.length() == 1) {
    680            return false;
    681        }
    682        final int pos = indexOfEqual(token);
    683        final String t = pos == -1 ? token : token.substring(0, pos);
    684        if (!getMatchingLongOptions(t).isEmpty()) {
    685            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
    686            return true;
    687        }
    688        if (getLongPrefix(token) != null && !token.startsWith(OptionFormatter.DEFAULT_LONG_OPT_PREFIX)) {
    689            // -LV
    690            return true;
    691        }
    692        return false;
    693    }
    694
    695    /**
    696     * Tests if the token is a negative number.
    697     *
    698     * @param token
    699     */
    700    private boolean isNegativeNumber(final String token) {
    701        try {
    702            Double.parseDouble(token);
    703            return true;
    704        } catch (final NumberFormatException e) {
    705            return false;
    706        }
    707    }
    708
    709    /**
    710     * Tests if the token looks like an option.
    711     *
    712     * @param token
    713     */
    714    private boolean isOption(final String token) {
    715        return isLongOption(token) || isShortOption(token);
    716    }
    717
    718    /**
    719     * Tests if the token looks like a short option.
    720     *
    721     * @param token
    722     */
    723    private boolean isShortOption(final String token) {
    724        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    725        if (token == null || !token.startsWith(OptionFormatter.DEFAULT_OPT_PREFIX) || token.length() == 1) {
    726            return false;
    727        }
    728        // remove leading "-" and "=value"
    729        final int pos = indexOfEqual(token);
    730        final String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
    731        if (options.hasShortOption(optName)) {
    732            return true;
    733        }
    734        // check for several concatenated short options
    735        return !optName.isEmpty() && options.hasShortOption(String.valueOf(optName.charAt(0)));
    736    }
    737
    738    /**
    739     * Parses the arguments according to the specified options and properties.
    740     *
    741     * @param options the specified Options
    742     * @param properties command line option name-value pairs
    743     * @param nonOptionAction see {@link NonOptionAction}.
    744     * @param arguments the command line arguments
    745     *
    746     * @return the list of atomic option and value tokens.
    747     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
    748     * @since 1.10.0
    749     */
    750    public CommandLine parse(final Options options, final Properties properties, final NonOptionAction nonOptionAction, final String... arguments)
    751            throws ParseException {
    752        this.options = Objects.requireNonNull(options, "options");
    753        this.nonOptionAction = nonOptionAction;
    754        skipParsing = false;
    755        currentOption = null;
    756        expectedOpts = new ArrayList<>(options.getRequiredOptions());
    757        // clear the data from the groups
    758        for (final OptionGroup optionGroup : options.getOptionGroups()) {
    759            optionGroup.setSelected(null);
    760        }
    761        cmd = CommandLine.builder().setDeprecatedHandler(deprecatedHandler).get();
    762        if (arguments != null) {
    763            for (final String argument : arguments) {
    764                handleToken(argument);
    765            }
    766        }
    767        // check the arguments of the last option
    768        checkRequiredArgs();
    769        // add the default options
    770        handleProperties(properties);
    771        checkRequiredOptions();
    772        return cmd;
    773    }
    774
    775    @Override
    776    public CommandLine parse(final Options options, final String[] arguments) throws ParseException {
    777        return parse(options, arguments, null);
    778    }
    779
    780    /**
    781     * @see #parse(Options, Properties, NonOptionAction, String[])
    782     */
    783    @Override
    784    public CommandLine parse(final Options options, final String[] arguments, final boolean stopAtNonOption) throws ParseException {
    785        return parse(options, arguments, null, stopAtNonOption);
    786    }
    787
    788    /**
    789     * Parses the arguments according to the specified options and properties.
    790     *
    791     * @param options the specified Options.
    792     * @param arguments the command line arguments.
    793     * @param properties command line option name-value pairs.
    794     * @return the list of atomic option and value tokens.
    795     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
    796     */
    797    public CommandLine parse(final Options options, final String[] arguments, final Properties properties) throws ParseException {
    798        return parse(options, arguments, properties, false);
    799    }
    800
    801    /**
    802     * Parses the arguments according to the specified options and properties.
    803     *
    804     * @param options the specified Options.
    805     * @param arguments the command line arguments.
    806     * @param properties command line option name-value pairs.
    807     * @param stopAtNonOption if {@code true} an unrecognized argument stops the parsing and the remaining arguments
    808     *        are added to the {@link CommandLine}s args list. If {@code false} an unrecognized argument triggers a
    809     *        ParseException.
    810     * @return the list of atomic option and value tokens.
    811     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
    812     * @see #parse(Options, Properties, NonOptionAction, String[])
    813     */
    814    public CommandLine parse(final Options options, final String[] arguments, final Properties properties, final boolean stopAtNonOption)
    815        throws ParseException {
    816        return parse(options, properties, stopAtNonOption ? NonOptionAction.STOP : NonOptionAction.THROW, arguments);
    817    }
    818
    819    /**
    820     * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
    821     * If stripLeadingAndTrailingQuotes is null, then do not strip
    822     *
    823     * @param token a string.
    824     * @return token with the quotes stripped (if set).
    825     */
    826    private String stripLeadingAndTrailingQuotesDefaultOff(final String token) {
    827        if (stripLeadingAndTrailingQuotes != null && stripLeadingAndTrailingQuotes) {
    828            return Util.stripLeadingAndTrailingQuotes(token);
    829        }
    830        return token;
    831    }
    832
    833    /**
    834     * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
    835     * If stripLeadingAndTrailingQuotes is null, then do not strip
    836     *
    837     * @param token a string.
    838     * @return token with the quotes stripped (if set).
    839     */
    840    private String stripLeadingAndTrailingQuotesDefaultOn(final String token) {
    841        if (stripLeadingAndTrailingQuotes == null || stripLeadingAndTrailingQuotes) {
    842            return Util.stripLeadingAndTrailingQuotes(token);
    843        }
    844        return token;
    845    }
    846
    847    /**
    848     * Removes the option or its group from the list of expected elements.
    849     *
    850     * @param option
    851     */
    852    private void updateRequiredOptions(final Option option) throws AlreadySelectedException {
    853        if (option.isRequired()) {
    854            expectedOpts.remove(option.getKey());
    855        }
    856        // if the option is in an OptionGroup make that option the selected option of the group
    857        if (options.getOptionGroup(option) != null) {
    858            final OptionGroup optionGroup = options.getOptionGroup(option);
    859            if (optionGroup.isRequired()) {
    860                expectedOpts.remove(optionGroup);
    861            }
    862            optionGroup.setSelected(option);
    863        }
    864    }
    865}