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