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: DefaultParser.java 1443102 2013-02-06 18:12:16Z tn $
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 analysed 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(Options options, 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(Options options, String[] arguments, Properties properties) throws ParseException
75 {
76 return parse(options, arguments, properties, false);
77 }
78
79 public CommandLine parse(Options options, String[] arguments, 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(Options options, String[] arguments, Properties properties, 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 (OptionGroup group : options.getOptionGroups())
110 {
111 group.setSelected(null);
112 }
113
114 cmd = new CommandLine();
115
116 if (arguments != null)
117 {
118 for (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(Properties properties) throws ParseException
141 {
142 if (properties == null)
143 {
144 return;
145 }
146
147 for (Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();)
148 {
149 String option = e.nextElement().toString();
150
151 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 OptionGroup group = options.getOptionGroup(opt);
159 boolean selected = group != null && group.getSelected() != null;
160
161 if (!cmd.hasOption(option) && !selected)
162 {
163 // get the value from the properties
164 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 private 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(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(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(String token)
272 {
273 try
274 {
275 Double.parseDouble(token);
276 return true;
277 }
278 catch (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(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(String token)
300 {
301 // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
302 return token.startsWith("-") && token.length() >= 2 && options.hasShortOption(token.substring(1, 2));
303 }
304
305 /**
306 * Tells if the token looks like a long option.
307 *
308 * @param token
309 */
310 private boolean isLongOption(String token)
311 {
312 if (!token.startsWith("-") || token.length() == 1)
313 {
314 return false;
315 }
316
317 int pos = token.indexOf("=");
318 String t = pos == -1 ? token : token.substring(0, pos);
319
320 if (!options.getMatchingOptions(t).isEmpty())
321 {
322 // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
323 return true;
324 }
325 else if (getLongPrefix(token) != null && !token.startsWith("--"))
326 {
327 // -LV
328 return true;
329 }
330
331 return false;
332 }
333
334 /**
335 * Handles an unknown token. If the token starts with a dash an
336 * UnrecognizedOptionException is thrown. Otherwise the token is added
337 * to the arguments of the command line. If the stopAtNonOption flag
338 * is set, this stops the parsing and the remaining tokens are added
339 * as-is in the arguments of the command line.
340 *
341 * @param token the command line token to handle
342 */
343 private void handleUnknownToken(String token) throws ParseException
344 {
345 if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption)
346 {
347 throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
348 }
349
350 cmd.addArg(token);
351 if (stopAtNonOption)
352 {
353 skipParsing = true;
354 }
355 }
356
357 /**
358 * Handles the following tokens:
359 *
360 * --L
361 * --L=V
362 * --L V
363 * --l
364 *
365 * @param token the command line token to handle
366 */
367 private void handleLongOption(String token) throws ParseException
368 {
369 if (token.indexOf('=') == -1)
370 {
371 handleLongOptionWithoutEqual(token);
372 }
373 else
374 {
375 handleLongOptionWithEqual(token);
376 }
377 }
378
379 /**
380 * Handles the following tokens:
381 *
382 * --L
383 * -L
384 * --l
385 * -l
386 *
387 * @param token the command line token to handle
388 */
389 private void handleLongOptionWithoutEqual(String token) throws ParseException
390 {
391 List<String> matchingOpts = options.getMatchingOptions(token);
392 if (matchingOpts.isEmpty())
393 {
394 handleUnknownToken(currentToken);
395 }
396 else if (matchingOpts.size() > 1)
397 {
398 throw new AmbiguousOptionException(token, matchingOpts);
399 }
400 else
401 {
402 handleOption(options.getOption(matchingOpts.get(0)));
403 }
404 }
405
406 /**
407 * Handles the following tokens:
408 *
409 * --L=V
410 * -L=V
411 * --l=V
412 * -l=V
413 *
414 * @param token the command line token to handle
415 */
416 private void handleLongOptionWithEqual(String token) throws ParseException
417 {
418 int pos = token.indexOf('=');
419
420 String value = token.substring(pos + 1);
421
422 String opt = token.substring(0, pos);
423
424 List<String> matchingOpts = options.getMatchingOptions(opt);
425 if (matchingOpts.isEmpty())
426 {
427 handleUnknownToken(currentToken);
428 }
429 else if (matchingOpts.size() > 1)
430 {
431 throw new AmbiguousOptionException(opt, matchingOpts);
432 }
433 else
434 {
435 Option option = options.getOption(matchingOpts.get(0));
436
437 if (option.acceptsArg())
438 {
439 handleOption(option);
440 currentOption.addValueForProcessing(value);
441 currentOption = null;
442 }
443 else
444 {
445 handleUnknownToken(currentToken);
446 }
447 }
448 }
449
450 /**
451 * Handles the following tokens:
452 *
453 * -S
454 * -SV
455 * -S V
456 * -S=V
457 * -S1S2
458 * -S1S2 V
459 * -SV1=V2
460 *
461 * -L
462 * -LV
463 * -L V
464 * -L=V
465 * -l
466 *
467 * @param token the command line token to handle
468 */
469 private void handleShortAndLongOption(String token) throws ParseException
470 {
471 String t = Util.stripLeadingHyphens(token);
472
473 int pos = t.indexOf('=');
474
475 if (t.length() == 1)
476 {
477 // -S
478 if (options.hasShortOption(t))
479 {
480 handleOption(options.getOption(t));
481 }
482 else
483 {
484 handleUnknownToken(token);
485 }
486 }
487 else if (pos == -1)
488 {
489 // no equal sign found (-xxx)
490 if (options.hasShortOption(t))
491 {
492 handleOption(options.getOption(t));
493 }
494 else if (!options.getMatchingOptions(t).isEmpty())
495 {
496 // -L or -l
497 handleLongOptionWithoutEqual(token);
498 }
499 else
500 {
501 // look for a long prefix (-Xmx512m)
502 String opt = getLongPrefix(t);
503
504 if (opt != null && options.getOption(opt).acceptsArg())
505 {
506 handleOption(options.getOption(opt));
507 currentOption.addValueForProcessing(t.substring(opt.length()));
508 currentOption = null;
509 }
510 else if (isJavaProperty(t))
511 {
512 // -SV1 (-Dflag)
513 handleOption(options.getOption(t.substring(0, 1)));
514 currentOption.addValueForProcessing(t.substring(1));
515 currentOption = null;
516 }
517 else
518 {
519 // -S1S2S3 or -S1S2V
520 handleConcatenatedOptions(token);
521 }
522 }
523 }
524 else
525 {
526 // equal sign found (-xxx=yyy)
527 String opt = t.substring(0, pos);
528 String value = t.substring(pos + 1);
529
530 if (opt.length() == 1)
531 {
532 // -S=V
533 Option option = options.getOption(opt);
534 if (option != null && option.acceptsArg())
535 {
536 handleOption(option);
537 currentOption.addValueForProcessing(value);
538 currentOption = null;
539 }
540 else
541 {
542 handleUnknownToken(token);
543 }
544 }
545 else if (isJavaProperty(opt))
546 {
547 // -SV1=V2 (-Dkey=value)
548 handleOption(options.getOption(opt.substring(0, 1)));
549 currentOption.addValueForProcessing(opt.substring(1));
550 currentOption.addValueForProcessing(value);
551 currentOption = null;
552 }
553 else
554 {
555 // -L=V or -l=V
556 handleLongOptionWithEqual(token);
557 }
558 }
559 }
560
561 /**
562 * Search for a prefix that is the long name of an option (-Xmx512m)
563 *
564 * @param token
565 */
566 private String getLongPrefix(String token)
567 {
568 String t = Util.stripLeadingHyphens(token);
569
570 int i;
571 String opt = null;
572 for (i = t.length() - 2; i > 1; i--)
573 {
574 String prefix = t.substring(0, i);
575 if (options.hasLongOption(prefix))
576 {
577 opt = prefix;
578 break;
579 }
580 }
581
582 return opt;
583 }
584
585 /**
586 * Check if the specified token is a Java-like property (-Dkey=value).
587 */
588 private boolean isJavaProperty(String token)
589 {
590 String opt = token.substring(0, 1);
591 Option option = options.getOption(opt);
592
593 return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
594 }
595
596 private void handleOption(Option option) throws ParseException
597 {
598 // check the previous option before handling the next one
599 checkRequiredArgs();
600
601 option = (Option) option.clone();
602
603 updateRequiredOptions(option);
604
605 cmd.addOption(option);
606
607 if (option.hasArg())
608 {
609 currentOption = option;
610 }
611 else
612 {
613 currentOption = null;
614 }
615 }
616
617 /**
618 * Removes the option or its group from the list of expected elements.
619 *
620 * @param option
621 */
622 private void updateRequiredOptions(Option option) throws AlreadySelectedException
623 {
624 if (option.isRequired())
625 {
626 expectedOpts.remove(option.getKey());
627 }
628
629 // if the option is in an OptionGroup make that option the selected option of the group
630 if (options.getOptionGroup(option) != null)
631 {
632 OptionGroup group = options.getOptionGroup(option);
633
634 if (group.isRequired())
635 {
636 expectedOpts.remove(group);
637 }
638
639 group.setSelected(option);
640 }
641 }
642
643 /**
644 * Breaks <code>token</code> into its constituent parts
645 * using the following algorithm.
646 *
647 * <ul>
648 * <li>ignore the first character ("<b>-</b>")</li>
649 * <li>foreach remaining character check if an {@link Option}
650 * exists with that id.</li>
651 * <li>if an {@link Option} does exist then add that character
652 * prepended with "<b>-</b>" to the list of processed tokens.</li>
653 * <li>if the {@link Option} can have an argument value and there
654 * are remaining characters in the token then add the remaining
655 * characters as a token to the list of processed tokens.</li>
656 * <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
657 * <code>stopAtNonOption</code> <b>IS</b> set then add the special token
658 * "<b>--</b>" followed by the remaining characters and also
659 * the remaining tokens directly to the processed tokens list.</li>
660 * <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
661 * <code>stopAtNonOption</code> <b>IS NOT</b> set then add that
662 * character prepended with "<b>-</b>".</li>
663 * </ul>
664 *
665 * @param token The current token to be <b>burst</b>
666 * at the first non-Option encountered.
667 */
668 protected void handleConcatenatedOptions(String token) throws ParseException
669 {
670 for (int i = 1; i < token.length(); i++)
671 {
672 String ch = String.valueOf(token.charAt(i));
673
674 if (options.hasOption(ch))
675 {
676 handleOption(options.getOption(ch));
677
678 if (currentOption != null && (token.length() != (i + 1)))
679 {
680 // add the trail as an argument of the option
681 currentOption.addValueForProcessing(token.substring(i + 1));
682 break;
683 }
684 }
685 else
686 {
687 handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);
688 break;
689 }
690 }
691 }
692 }