DefaultParser.java

  1. /*
  2.   Licensed to the Apache Software Foundation (ASF) under one or more
  3.   contributor license agreements.  See the NOTICE file distributed with
  4.   this work for additional information regarding copyright ownership.
  5.   The ASF licenses this file to You under the Apache License, Version 2.0
  6.   (the "License"); you may not use this file except in compliance with
  7.   the License.  You may obtain a copy of the License at

  8.       https://www.apache.org/licenses/LICENSE-2.0

  9.   Unless required by applicable law or agreed to in writing, software
  10.   distributed under the License is distributed on an "AS IS" BASIS,
  11.   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12.   See the License for the specific language governing permissions and
  13.   limitations under the License.
  14.  */

  15. package org.apache.commons.cli;

  16. import java.util.ArrayList;
  17. import java.util.Enumeration;
  18. import java.util.List;
  19. import java.util.Objects;
  20. import java.util.Properties;
  21. import java.util.function.Consumer;
  22. import java.util.function.Supplier;

  23. /**
  24.  * Default parser.
  25.  *
  26.  * @since 1.3
  27.  */
  28. public class DefaultParser implements CommandLineParser {

  29.     /**
  30.      * A nested builder class to create {@code DefaultParser} instances
  31.      * using descriptive methods.
  32.      *
  33.      * Example usage:
  34.      * <pre>
  35.      * DefaultParser parser = Option.builder()
  36.      *     .setAllowPartialMatching(false)
  37.      *     .setStripLeadingAndTrailingQuotes(false)
  38.      *     .build();
  39.      * </pre>
  40.      *
  41.      * @since 1.5.0
  42.      */
  43.     public static final class Builder implements Supplier<DefaultParser> {

  44.         /** Flag indicating if partial matching of long options is supported. */
  45.         private boolean allowPartialMatching = true;

  46.         /**
  47.          * The deprecated option handler.
  48.          * <p>
  49.          * If you want to serialize this field, use a serialization proxy.
  50.          * </p>
  51.          */
  52.         private Consumer<Option> deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;

  53.         /** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments. */
  54.         private Boolean stripLeadingAndTrailingQuotes;

  55.         /**
  56.          * Constructs a new {@code Builder} for a {@code DefaultParser} instance.
  57.          *
  58.          * Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by default,
  59.          * mimicking the argument-less constructor.
  60.          */
  61.         private Builder() {
  62.         }

  63.         /**
  64.          * Builds an DefaultParser with the values declared by this {@link Builder}.
  65.          *
  66.          * @return the new {@link DefaultParser}.
  67.          * @since 1.5.0
  68.          * @deprecated Use {@link #get()}.
  69.          */
  70.         @Deprecated
  71.         public DefaultParser build() {
  72.             return get();
  73.         }

  74.         /**
  75.          * Builds an DefaultParser with the values declared by this {@link Builder}.
  76.          *
  77.          * @return the new {@link DefaultParser}.
  78.          * @since 1.10.0
  79.          */
  80.         @Override
  81.         public DefaultParser get() {
  82.             return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes, deprecatedHandler);
  83.         }

  84.         /**
  85.          * Sets if partial matching of long options is supported.
  86.          *
  87.          * By "partial matching" we mean that given the following code:
  88.          *
  89.          * <pre>
  90.          * {
  91.          *     &#64;code
  92.          *     final Options options = new Options();
  93.          *     options.addOption(new Option("d", "debug", false, "Turn on debug."));
  94.          *     options.addOption(new Option("e", "extract", false, "Turn on extract."));
  95.          *     options.addOption(new Option("o", "option", true, "Turn on option with argument."));
  96.          * }
  97.          * </pre>
  98.          *
  99.          * If "partial matching" is turned on, {@code -de} only matches the {@code "debug"} option. However, with
  100.          * "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
  101.          *
  102.          * @param allowPartialMatching whether to allow partial matching of long options.
  103.          * @return {@code this} instance..
  104.          * @since 1.5.0
  105.          */
  106.         public Builder setAllowPartialMatching(final boolean allowPartialMatching) {
  107.             this.allowPartialMatching = allowPartialMatching;
  108.             return this;
  109.         }

  110.         /**
  111.          * Sets the deprecated option handler.
  112.          *
  113.          * @param deprecatedHandler the deprecated option handler.
  114.          * @return {@code this} instance.
  115.          * @since 1.7.0
  116.          */
  117.         public Builder setDeprecatedHandler(final Consumer<Option> deprecatedHandler) {
  118.             this.deprecatedHandler = deprecatedHandler;
  119.             return this;
  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 {@code this} instance.
  134.          * @since 1.5.0
  135.          */
  136.         public Builder setStripLeadingAndTrailingQuotes(final Boolean stripLeadingAndTrailingQuotes) {
  137.             this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
  138.             return this;
  139.         }
  140.     }

  141.     /**
  142.      * Enum representing possible actions that may be done when "non option" is discovered during parsing.
  143.      *
  144.      * @since 1.10.0
  145.      */
  146.     public enum NonOptionAction {
  147.         /**
  148.          * Parsing continues and current token is ignored.
  149.          */
  150.         IGNORE,
  151.         /**
  152.          * Parsing continues and current token is added to command line arguments.
  153.          */
  154.         SKIP,
  155.         /**
  156.          * Parsing will stop and remaining tokens are added to command line arguments.
  157.          * Equivalent of {@code stopAtNonOption = true}.
  158.          */
  159.         STOP,
  160.         /**
  161.          * Parsing will abort and exception is thrown.
  162.          * Equivalent of {@code stopAtNonOption = false}.
  163.          */
  164.         THROW;
  165.     }

  166.     /**
  167.      * Creates a new {@link Builder} to create an {@link DefaultParser} using descriptive
  168.      * methods.
  169.      *
  170.      * @return a new {@link Builder} instance
  171.      * @since 1.5.0
  172.      */
  173.     public static Builder builder() {
  174.         return new Builder();
  175.     }

  176.     static int indexOfEqual(final String token) {
  177.         return token.indexOf(Char.EQUAL);
  178.     }

  179.     /** The command-line instance. */
  180.     protected CommandLine cmd;

  181.     /** The current options. */
  182.     protected Options options;

  183.     /**
  184.      * Flag indicating how unrecognized tokens are handled. {@code true} to stop the parsing and add the remaining
  185.      * tokens to the args list. {@code false} to throw an exception.
  186.      *
  187.      * @deprecated Use {@link #nonOptionAction} instead. This field is unused, and left for binary compatibility reasons.
  188.      */
  189.     @Deprecated
  190.     protected boolean stopAtNonOption;

  191.     /**
  192.      * Action to happen when "non option" token is discovered.
  193.      *
  194.      * @since 1.10.0
  195.      */
  196.     protected NonOptionAction nonOptionAction;

  197.     /** The token currently processed. */
  198.     protected String currentToken;

  199.     /** The last option parsed. */
  200.     protected Option currentOption;

  201.     /** Flag indicating if tokens should no longer be analyzed and simply added as arguments of the command line. */
  202.     protected boolean skipParsing;

  203.     /** The required options and groups expected to be found when parsing the command line. */
  204.     // This can contain either a String (addOption) or an OptionGroup (addOptionGroup)
  205.     // TODO this seems wrong
  206.     protected List expectedOpts;

  207.     /** Flag indicating if partial matching of long options is supported. */
  208.     private final boolean allowPartialMatching;

  209.     /** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments.
  210.      * null represents the historic arbitrary behavior */
  211.     private final Boolean stripLeadingAndTrailingQuotes;

  212.     /**
  213.      * The deprecated option handler.
  214.      * <p>
  215.      * If you want to serialize this field, use a serialization proxy.
  216.      * </p>
  217.      */
  218.     private final Consumer<Option> deprecatedHandler;

  219.     /**
  220.      * Creates a new DefaultParser instance with partial matching enabled.
  221.      *
  222.      * By "partial matching" we mean that given the following code:
  223.      *
  224.      * <pre>
  225.      * {
  226.      *     &#64;code
  227.      *     final Options options = new Options();
  228.      *     options.addOption(new Option("d", "debug", false, "Turn on debug."));
  229.      *     options.addOption(new Option("e", "extract", false, "Turn on extract."));
  230.      *     options.addOption(new Option("o", "option", true, "Turn on option with argument."));
  231.      * }
  232.      * </pre>
  233.      *
  234.      * with "partial matching" turned on, {@code -de} only matches the {@code "debug"} option. However, with
  235.      * "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
  236.      * options.
  237.      */
  238.     public DefaultParser() {
  239.         this.allowPartialMatching = true;
  240.         this.stripLeadingAndTrailingQuotes = null;
  241.         this.deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
  242.     }

  243.     /**
  244.      * Create a new DefaultParser instance with the specified partial matching policy.
  245.      *
  246.      * By "partial matching" we mean that given the following code:
  247.      *
  248.      * <pre>
  249.      * {
  250.      *     &#64;code
  251.      *     final Options options = new Options();
  252.      *     options.addOption(new Option("d", "debug", false, "Turn on debug."));
  253.      *     options.addOption(new Option("e", "extract", false, "Turn on extract."));
  254.      *     options.addOption(new Option("o", "option", true, "Turn on option with argument."));
  255.      * }
  256.      * </pre>
  257.      *
  258.      * with "partial matching" turned on, {@code -de} only matches the {@code "debug"} option. However, with
  259.      * "partial matching" disabled, {@code -de} would enable both {@code debug} as well as {@code extract}
  260.      * options.
  261.      *
  262.      * @param allowPartialMatching if partial matching of long options shall be enabled
  263.      */
  264.     public DefaultParser(final boolean allowPartialMatching) {
  265.         this.allowPartialMatching = allowPartialMatching;
  266.         this.stripLeadingAndTrailingQuotes = null;
  267.         this.deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
  268.     }

  269.     /**
  270.      * Creates a new DefaultParser instance with the specified partial matching and quote
  271.      * stripping policy.
  272.      *
  273.      * @param allowPartialMatching if partial matching of long options shall be enabled
  274.      * @param stripLeadingAndTrailingQuotes if balanced outer double quoutes should be stripped
  275.      */
  276.     private DefaultParser(final boolean allowPartialMatching, final Boolean stripLeadingAndTrailingQuotes, final Consumer<Option> deprecatedHandler) {
  277.         this.allowPartialMatching = allowPartialMatching;
  278.         this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
  279.         this.deprecatedHandler = deprecatedHandler;
  280.     }

  281.     /**
  282.      * Adds token to command line {@link CommandLine#addArg(String)}.
  283.      *
  284.      * @param token the unrecognized option/argument.
  285.      * @since 1.10.0
  286.      */
  287.     protected void addArg(final String token) {
  288.         cmd.addArg(token);
  289.     }

  290.     /**
  291.      * Throws a {@link MissingArgumentException} if the current option didn't receive the number of arguments expected.
  292.      */
  293.     private void checkRequiredArgs() throws ParseException {
  294.         if (currentOption != null && currentOption.requiresArg()) {
  295.             if (isJavaProperty(currentOption.getKey()) && currentOption.getValuesList().size() == 1) {
  296.                 return;
  297.             }
  298.             throw new MissingArgumentException(currentOption);
  299.         }
  300.     }

  301.     /**
  302.      * Throws a {@link MissingOptionException} if all of the required options are not present.
  303.      *
  304.      * @throws MissingOptionException if any of the required Options are not present.
  305.      */
  306.     protected void checkRequiredOptions() throws MissingOptionException {
  307.         // if there are required options that have not been processed
  308.         if (!expectedOpts.isEmpty()) {
  309.             throw new MissingOptionException(expectedOpts);
  310.         }
  311.     }

  312.     /**
  313.      * Searches for a prefix that is the long name of an option (-Xmx512m).
  314.      *
  315.      * @param token
  316.      */
  317.     private String getLongPrefix(final String token) {
  318.         final String t = Util.stripLeadingHyphens(token);
  319.         int i;
  320.         String opt = null;
  321.         for (i = t.length() - 2; i > 1; i--) {
  322.             final String prefix = t.substring(0, i);
  323.             if (options.hasLongOption(prefix)) {
  324.                 opt = prefix;
  325.                 break;
  326.             }
  327.         }
  328.         return opt;
  329.     }

  330.     /**
  331.      * Gets a list of matching option strings for the given token, depending on the selected partial matching policy.
  332.      *
  333.      * @param token the token (may contain leading dashes).
  334.      * @return the list of matching option strings or an empty list if no matching option could be found.
  335.      */
  336.     private List<String> getMatchingLongOptions(final String token) {
  337.         if (allowPartialMatching) {
  338.             return options.getMatchingOptions(token);
  339.         }
  340.         final List<String> matches = new ArrayList<>(1);
  341.         if (options.hasLongOption(token)) {
  342.             matches.add(options.getOption(token).getLongOpt());
  343.         }
  344.         return matches;
  345.     }

  346.     /**
  347.      * Breaks {@code token} into its constituent parts using the following algorithm.
  348.      *
  349.      * <ul>
  350.      * <li>ignore the first character ("<strong>-</strong>")</li>
  351.      * <li>for each remaining character check if an {@link Option} exists with that id.</li>
  352.      * <li>if an {@link Option} does exist then add that character prepended with "<strong>-</strong>" to the list of processed
  353.      * tokens.</li>
  354.      * <li>if the {@link Option} can have an argument value and there are remaining characters in the token then add the
  355.      * remaining characters as a token to the list of processed tokens.</li>
  356.      * <li>if an {@link Option} does <strong>NOT</strong> exist <strong>AND</strong> {@code stopAtNonOption} <strong>IS</strong> set then add the
  357.      * special token "<strong>--</strong>" followed by the remaining characters and also the remaining tokens directly to the
  358.      * processed tokens list.</li>
  359.      * <li>if an {@link Option} does <strong>NOT</strong> exist <strong>AND</strong> {@code stopAtNonOption} <strong>IS NOT</strong> set then add
  360.      * that character prepended with "<strong>-</strong>".</li>
  361.      * </ul>
  362.      *
  363.      * @param token The current token to be <strong>burst</strong> at the first non-Option encountered.
  364.      * @throws ParseException if there are any problems encountered while parsing the command line token.
  365.      */
  366.     protected void handleConcatenatedOptions(final String token) throws ParseException {
  367.         for (int i = 1; i < token.length(); i++) {
  368.             final String ch = String.valueOf(token.charAt(i));
  369.             if (!options.hasOption(ch)) {
  370.                 handleUnknownToken(nonOptionAction == NonOptionAction.STOP && i > 1 ? token.substring(i) : token);
  371.                 break;
  372.             }
  373.             handleOption(options.getOption(ch));
  374.             if (currentOption != null && token.length() != i + 1) {
  375.                 // add the trail as an argument of the option
  376.                 currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(i + 1)));
  377.                 break;
  378.             }
  379.         }
  380.     }

  381.     /**
  382.      * Handles the following tokens:
  383.      * <pre>
  384.      * --L --L=V --L V --l
  385.      * </pre>
  386.      *
  387.      * @param token the command line token to handle.
  388.      */
  389.     private void handleLongOption(final String token) throws ParseException {
  390.         if (indexOfEqual(token) == -1) {
  391.             handleLongOptionWithoutEqual(token);
  392.         } else {
  393.             handleLongOptionWithEqual(token);
  394.         }
  395.     }

  396.     /**
  397.      * Handles the following tokens:
  398.      * <pre>
  399.      * --L=V -L=V --l=V -l=V
  400.      * </pre>
  401.      *
  402.      * @param token the command line token to handle.
  403.      */
  404.     private void handleLongOptionWithEqual(final String token) throws ParseException {
  405.         final int pos = indexOfEqual(token);
  406.         final String value = token.substring(pos + 1);
  407.         final String opt = token.substring(0, pos);
  408.         final List<String> matchingOpts = getMatchingLongOptions(opt);
  409.         if (matchingOpts.isEmpty()) {
  410.             handleUnknownToken(currentToken);
  411.         } else if (matchingOpts.size() > 1 && !options.hasLongOption(opt)) {
  412.             throw new AmbiguousOptionException(opt, matchingOpts);
  413.         } else {
  414.             final String key = options.hasLongOption(opt) ? opt : matchingOpts.get(0);
  415.             final Option option = options.getOption(key);
  416.             if (option.acceptsArg()) {
  417.                 handleOption(option);
  418.                 currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(value));
  419.                 currentOption = null;
  420.             } else {
  421.                 handleUnknownToken(currentToken);
  422.             }
  423.         }
  424.     }

  425.     /**
  426.      * Handles the following tokens:
  427.      *
  428.      * <pre>
  429.      * --L -L --l -l
  430.      * </pre>
  431.      *
  432.      * @param token the command line token to handle.
  433.      */
  434.     private void handleLongOptionWithoutEqual(final String token) throws ParseException {
  435.         final List<String> matchingOpts = getMatchingLongOptions(token);
  436.         if (matchingOpts.isEmpty()) {
  437.             handleUnknownToken(currentToken);
  438.         } else if (matchingOpts.size() > 1 && !options.hasLongOption(token)) {
  439.             throw new AmbiguousOptionException(token, matchingOpts);
  440.         } else {
  441.             final String key = options.hasLongOption(token) ? token : matchingOpts.get(0);
  442.             handleOption(options.getOption(key));
  443.         }
  444.     }

  445.     private void handleOption(final Option option) throws ParseException {
  446.         // check the previous option before handling the next one
  447.         checkRequiredArgs();
  448.         final Option copy = (Option) option.clone();
  449.         updateRequiredOptions(copy);
  450.         cmd.addOption(copy);
  451.         currentOption = copy.hasArg() ? copy : null;
  452.     }

  453.     /**
  454.      * Sets the values of Options using the values in {@code properties}.
  455.      *
  456.      * @param properties The value properties to be processed.
  457.      */
  458.     private void handleProperties(final Properties properties) throws ParseException {
  459.         if (properties == null) {
  460.             return;
  461.         }
  462.         for (final Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
  463.             final String option = e.nextElement().toString();
  464.             final Option opt = options.getOption(option);
  465.             if (opt == null) {
  466.                 throw new UnrecognizedOptionException("Default option wasn't defined", option);
  467.             }
  468.             // if the option is part of a group, check if another option of the group has been selected
  469.             final OptionGroup optionGroup = options.getOptionGroup(opt);
  470.             final boolean selected = optionGroup != null && optionGroup.isSelected();
  471.             if (!cmd.hasOption(option) && !selected) {
  472.                 // get the value from the properties
  473.                 final String value = properties.getProperty(option);

  474.                 if (opt.hasArg()) {
  475.                     if (Util.isEmpty(opt.getValues())) {
  476.                         opt.processValue(stripLeadingAndTrailingQuotesDefaultOff(value));
  477.                     }
  478.                 } else if (!("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value))) {
  479.                     // if the value is not yes, true or 1 then don't add the option to the CommandLine
  480.                     continue;
  481.                 }
  482.                 handleOption(opt);
  483.                 currentOption = null;
  484.             }
  485.         }
  486.     }

  487.     /**
  488.      * Handles the following tokens:
  489.      * <pre>
  490.      * -S -SV -S V -S=V -S1S2 -S1S2 V -SV1=V2
  491.      *
  492.      * -L -LV -L V -L=V -l
  493.      * </pre>
  494.      *
  495.      * @param hyphenToken the command line token to handle.
  496.      */
  497.     private void handleShortAndLongOption(final String hyphenToken) throws ParseException {
  498.         final String token = Util.stripLeadingHyphens(hyphenToken);
  499.         final int pos = indexOfEqual(token);
  500.         if (token.length() == 1) {
  501.             // -S
  502.             if (options.hasShortOption(token)) {
  503.                 handleOption(options.getOption(token));
  504.             } else {
  505.                 handleUnknownToken(hyphenToken);
  506.             }
  507.         } else if (pos == -1) {
  508.             // no equal sign found (-xxx)
  509.             if (options.hasShortOption(token)) {
  510.                 handleOption(options.getOption(token));
  511.             } else if (!getMatchingLongOptions(token).isEmpty()) {
  512.                 // -L or -l
  513.                 handleLongOptionWithoutEqual(hyphenToken);
  514.             } else {
  515.                 // look for a long prefix (-Xmx512m)
  516.                 final String opt = getLongPrefix(token);

  517.                 if (opt != null && options.getOption(opt).acceptsArg()) {
  518.                     handleOption(options.getOption(opt));
  519.                     currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(opt.length())));
  520.                     currentOption = null;
  521.                 } else if (isJavaProperty(token)) {
  522.                     // -SV1 (-Dflag)
  523.                     handleOption(options.getOption(token.substring(0, 1)));
  524.                     currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOff(token.substring(1)));
  525.                     currentOption = null;
  526.                 } else {
  527.                     // -S1S2S3 or -S1S2V
  528.                     handleConcatenatedOptions(hyphenToken);
  529.                 }
  530.             }
  531.         } else {
  532.             // equal sign found (-xxx=yyy)
  533.             final String opt = token.substring(0, pos);
  534.             final String value = token.substring(pos + 1);

  535.             if (opt.length() == 1) {
  536.                 // -S=V
  537.                 final Option option = options.getOption(opt);
  538.                 if (option != null && option.acceptsArg()) {
  539.                     handleOption(option);
  540.                     currentOption.processValue(value);
  541.                     currentOption = null;
  542.                 } else {
  543.                     handleUnknownToken(hyphenToken);
  544.                 }
  545.             } else if (isJavaProperty(opt)) {
  546.                 // -SV1=V2 (-Dkey=value)
  547.                 handleOption(options.getOption(opt.substring(0, 1)));
  548.                 currentOption.processValue(opt.substring(1));
  549.                 currentOption.processValue(value);
  550.                 currentOption = null;
  551.             } else {
  552.                 // -L=V or -l=V
  553.                 handleLongOptionWithEqual(hyphenToken);
  554.             }
  555.         }
  556.     }

  557.     /**
  558.      * Handles any command line token.
  559.      *
  560.      * @param token the command line token to handle.
  561.      * @throws ParseException
  562.      */
  563.     private void handleToken(final String token) throws ParseException {
  564.         if (token != null) {
  565.             currentToken = token;
  566.             if (skipParsing) {
  567.                 addArg(token);
  568.             } else if ("--".equals(token)) {
  569.                 skipParsing = true;
  570.             } else if (currentOption != null && currentOption.acceptsArg() && isArgument(token)) {
  571.                 currentOption.processValue(stripLeadingAndTrailingQuotesDefaultOn(token));
  572.             } else if (token.startsWith("--")) {
  573.                 handleLongOption(token);
  574.             } else if (token.startsWith("-") && !"-".equals(token)) {
  575.                 handleShortAndLongOption(token);
  576.             } else {
  577.                 handleUnknownToken(token);
  578.             }
  579.             if (currentOption != null && !currentOption.acceptsArg()) {
  580.                 currentOption = null;
  581.             }
  582.         }
  583.     }

  584.     /**
  585.      * Handles an unknown token. If the token starts with a dash an UnrecognizedOptionException is thrown. Otherwise the
  586.      * token is added to the arguments of the command line. If the stopAtNonOption flag is set, this stops the parsing and
  587.      * the remaining tokens are added as-is in the arguments of the command line.
  588.      *
  589.      * @param token the command line token to handle.
  590.      * @throws ParseException if parsing should fail.
  591.      * @since 1.10.0
  592.      */
  593.     protected void handleUnknownToken(final String token) throws ParseException {
  594.         if (token.startsWith("-") && token.length() > 1 && nonOptionAction == NonOptionAction.THROW) {
  595.             throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
  596.         }
  597.         if (!token.startsWith("-") || token.equals("-") || token.length() > 1 && nonOptionAction != NonOptionAction.IGNORE) {
  598.             addArg(token);
  599.         }
  600.         if (nonOptionAction == NonOptionAction.STOP) {
  601.             skipParsing = true;
  602.         }
  603.     }

  604.     /**
  605.      * Tests if the token is a valid argument.
  606.      *
  607.      * @param token
  608.      */
  609.     private boolean isArgument(final String token) {
  610.         return !isOption(token) || isNegativeNumber(token);
  611.     }

  612.     /**
  613.      * Tests if the specified token is a Java-like property (-Dkey=value).
  614.      */
  615.     private boolean isJavaProperty(final String token) {
  616.         final String opt = token.isEmpty() ? null : token.substring(0, 1);
  617.         final Option option = options.getOption(opt);
  618.         return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
  619.     }

  620.     /**
  621.      * Tests if the token looks like a long option.
  622.      *
  623.      * @param token
  624.      */
  625.     private boolean isLongOption(final String token) {
  626.         if (token == null || !token.startsWith("-") || token.length() == 1) {
  627.             return false;
  628.         }
  629.         final int pos = indexOfEqual(token);
  630.         final String t = pos == -1 ? token : token.substring(0, pos);
  631.         if (!getMatchingLongOptions(t).isEmpty()) {
  632.             // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
  633.             return true;
  634.         }
  635.         if (getLongPrefix(token) != null && !token.startsWith("--")) {
  636.             // -LV
  637.             return true;
  638.         }
  639.         return false;
  640.     }

  641.     /**
  642.      * Tests if the token is a negative number.
  643.      *
  644.      * @param token
  645.      */
  646.     private boolean isNegativeNumber(final String token) {
  647.         try {
  648.             Double.parseDouble(token);
  649.             return true;
  650.         } catch (final NumberFormatException e) {
  651.             return false;
  652.         }
  653.     }

  654.     /**
  655.      * Tests if the token looks like an option.
  656.      *
  657.      * @param token
  658.      */
  659.     private boolean isOption(final String token) {
  660.         return isLongOption(token) || isShortOption(token);
  661.     }

  662.     /**
  663.      * Tests if the token looks like a short option.
  664.      *
  665.      * @param token
  666.      */
  667.     private boolean isShortOption(final String token) {
  668.         // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
  669.         if (token == null || !token.startsWith("-") || token.length() == 1) {
  670.             return false;
  671.         }
  672.         // remove leading "-" and "=value"
  673.         final int pos = indexOfEqual(token);
  674.         final String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
  675.         if (options.hasShortOption(optName)) {
  676.             return true;
  677.         }
  678.         // check for several concatenated short options
  679.         return !optName.isEmpty() && options.hasShortOption(String.valueOf(optName.charAt(0)));
  680.     }

  681.     /**
  682.      * Parses the arguments according to the specified options and properties.
  683.      *
  684.      * @param options the specified Options
  685.      * @param properties command line option name-value pairs
  686.      * @param nonOptionAction see {@link NonOptionAction}.
  687.      * @param arguments the command line arguments
  688.      *
  689.      * @return the list of atomic option and value tokens.
  690.      * @throws ParseException if there are any problems encountered while parsing the command line tokens.
  691.      * @since 1.10.0
  692.      */
  693.     public CommandLine parse(final Options options, final Properties properties, final NonOptionAction nonOptionAction, final String... arguments)
  694.             throws ParseException {
  695.         this.options = Objects.requireNonNull(options, "options");
  696.         this.nonOptionAction = nonOptionAction;
  697.         skipParsing = false;
  698.         currentOption = null;
  699.         expectedOpts = new ArrayList<>(options.getRequiredOptions());
  700.         // clear the data from the groups
  701.         for (final OptionGroup optionGroup : options.getOptionGroups()) {
  702.             optionGroup.setSelected(null);
  703.         }
  704.         cmd = CommandLine.builder().setDeprecatedHandler(deprecatedHandler).get();
  705.         if (arguments != null) {
  706.             for (final String argument : arguments) {
  707.                 handleToken(argument);
  708.             }
  709.         }
  710.         // check the arguments of the last option
  711.         checkRequiredArgs();
  712.         // add the default options
  713.         handleProperties(properties);
  714.         checkRequiredOptions();
  715.         return cmd;
  716.     }

  717.     @Override
  718.     public CommandLine parse(final Options options, final String[] arguments) throws ParseException {
  719.         return parse(options, arguments, null);
  720.     }

  721.     /**
  722.      * @see #parse(Options, Properties, NonOptionAction, String[])
  723.      */
  724.     @Override
  725.     public CommandLine parse(final Options options, final String[] arguments, final boolean stopAtNonOption) throws ParseException {
  726.         return parse(options, arguments, null, stopAtNonOption);
  727.     }

  728.     /**
  729.      * Parses the arguments according to the specified options and properties.
  730.      *
  731.      * @param options the specified Options.
  732.      * @param arguments the command line arguments.
  733.      * @param properties command line option name-value pairs.
  734.      * @return the list of atomic option and value tokens.
  735.      * @throws ParseException if there are any problems encountered while parsing the command line tokens.
  736.      */
  737.     public CommandLine parse(final Options options, final String[] arguments, final Properties properties) throws ParseException {
  738.         return parse(options, arguments, properties, false);
  739.     }

  740.     /**
  741.      * Parses the arguments according to the specified options and properties.
  742.      *
  743.      * @param options the specified Options.
  744.      * @param arguments the command line arguments.
  745.      * @param properties command line option name-value pairs.
  746.      * @param stopAtNonOption if {@code true} an unrecognized argument stops the parsing and the remaining arguments
  747.      *        are added to the {@link CommandLine}s args list. If {@code false} an unrecognized argument triggers a
  748.      *        ParseException.
  749.      * @return the list of atomic option and value tokens.
  750.      * @throws ParseException if there are any problems encountered while parsing the command line tokens.
  751.      * @see #parse(Options, Properties, NonOptionAction, String[])
  752.      */
  753.     public CommandLine parse(final Options options, final String[] arguments, final Properties properties, final boolean stopAtNonOption)
  754.         throws ParseException {
  755.         return parse(options, properties, stopAtNonOption ? NonOptionAction.STOP : NonOptionAction.THROW, arguments);
  756.     }

  757.     /**
  758.      * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
  759.      * If stripLeadingAndTrailingQuotes is null, then do not strip
  760.      *
  761.      * @param token a string.
  762.      * @return token with the quotes stripped (if set).
  763.      */
  764.     private String stripLeadingAndTrailingQuotesDefaultOff(final String token) {
  765.         if (stripLeadingAndTrailingQuotes != null && stripLeadingAndTrailingQuotes) {
  766.             return Util.stripLeadingAndTrailingQuotes(token);
  767.         }
  768.         return token;
  769.     }

  770.     /**
  771.      * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
  772.      * If stripLeadingAndTrailingQuotes is null, then do not strip
  773.      *
  774.      * @param token a string.
  775.      * @return token with the quotes stripped (if set).
  776.      */
  777.     private String stripLeadingAndTrailingQuotesDefaultOn(final String token) {
  778.         if (stripLeadingAndTrailingQuotes == null || stripLeadingAndTrailingQuotes) {
  779.             return Util.stripLeadingAndTrailingQuotes(token);
  780.         }
  781.         return token;
  782.     }

  783.     /**
  784.      * Removes the option or its group from the list of expected elements.
  785.      *
  786.      * @param option
  787.      */
  788.     private void updateRequiredOptions(final Option option) throws AlreadySelectedException {
  789.         if (option.isRequired()) {
  790.             expectedOpts.remove(option.getKey());
  791.         }
  792.         // if the option is in an OptionGroup make that option the selected option of the group
  793.         if (options.getOptionGroup(option) != null) {
  794.             final OptionGroup optionGroup = options.getOptionGroup(option);
  795.             if (optionGroup.isRequired()) {
  796.                 expectedOpts.remove(optionGroup);
  797.             }
  798.             optionGroup.setSelected(option);
  799.         }
  800.     }
  801. }