View Javadoc
1   /*
2     Licensed to the Apache Software Foundation (ASF) under one or more
3     contributor license agreements.  See the NOTICE file distributed with
4     this work for additional information regarding copyright ownership.
5     The ASF licenses this file to You under the Apache License, Version 2.0
6     (the "License"); you may not use this file except in compliance with
7     the License.  You may obtain a copy of the License at
8   
9         https://www.apache.org/licenses/LICENSE-2.0
10  
11    Unless required by applicable law or agreed to in writing, software
12    distributed under the License is distributed on an "AS IS" BASIS,
13    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14    See the License for the specific language governing permissions and
15    limitations under the License.
16   */
17  
18  package org.apache.commons.cli;
19  
20  import java.util.ArrayList;
21  import java.util.Enumeration;
22  import java.util.List;
23  import java.util.Objects;
24  import java.util.Properties;
25  import java.util.function.Consumer;
26  import java.util.function.Supplier;
27  
28  import org.apache.commons.cli.help.OptionFormatter;
29  
30  /**
31   * Default parser.
32   *
33   * @since 1.3
34   */
35  public class DefaultParser implements CommandLineParser {
36  
37      /**
38       * A nested builder class to create {@code DefaultParser} instances
39       * using descriptive methods.
40       *
41       * Example usage:
42       * <pre>
43       * DefaultParser parser = Option.builder()
44       *     .setAllowPartialMatching(false)
45       *     .setStripLeadingAndTrailingQuotes(false)
46       *     .build();
47       * </pre>
48       *
49       * @since 1.5.0
50       */
51      public static final class Builder implements Supplier<DefaultParser> {
52  
53          /** Flag indicating if partial matching of long options is supported. */
54          private boolean allowPartialMatching = true;
55  
56          /**
57           * The deprecated option handler.
58           * <p>
59           * If you want to serialize this field, use a serialization proxy.
60           * </p>
61           */
62          private Consumer<Option> deprecatedHandler = CommandLine.Builder.DEPRECATED_HANDLER;
63  
64          /** Flag indicating if balanced leading and trailing double quotes should be stripped from option arguments. */
65          private Boolean stripLeadingAndTrailingQuotes;
66  
67          /**
68           * Constructs a new {@code Builder} for a {@code DefaultParser} instance.
69           * <p>
70           * Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by default, mimicking the argument-less constructor.
71           * </p>
72           */
73          private Builder() {
74          }
75  
76          /**
77           * Builds an DefaultParser with the values declared by this {@link Builder}.
78           *
79           * @return the new {@link DefaultParser}.
80           * @since 1.5.0
81           * @deprecated Use {@link #get()}.
82           */
83          @Deprecated
84          public DefaultParser build() {
85              return get();
86          }
87  
88          /**
89           * Builds an DefaultParser with the values declared by this {@link Builder}.
90           *
91           * @return the new {@link DefaultParser}.
92           * @since 1.10.0
93           */
94          @Override
95          public DefaultParser get() {
96              return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes, deprecatedHandler);
97          }
98  
99          /**
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 }