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