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