001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.cli; 019 020import java.util.ArrayList; 021import java.util.Enumeration; 022import java.util.List; 023import java.util.Properties; 024 025/** 026 * Default parser. 027 * 028 * @version $Id: DefaultParser.java 1783175 2017-02-16 07:52:05Z britter $ 029 * @since 1.3 030 */ 031public class DefaultParser implements CommandLineParser 032{ 033 /** The command-line instance. */ 034 protected CommandLine cmd; 035 036 /** The current options. */ 037 protected Options options; 038 039 /** 040 * Flag indicating how unrecognized tokens are handled. <tt>true</tt> to stop 041 * the parsing and add the remaining tokens to the args list. 042 * <tt>false</tt> to throw an exception. 043 */ 044 protected boolean stopAtNonOption; 045 046 /** The token currently processed. */ 047 protected String currentToken; 048 049 /** The last option parsed. */ 050 protected Option currentOption; 051 052 /** Flag indicating if tokens should no longer be analyzed and simply added as arguments of the command line. */ 053 protected boolean skipParsing; 054 055 /** The required options and groups expected to be found when parsing the command line. */ 056 protected List expectedOpts; 057 058 public CommandLine parse(Options options, String[] arguments) throws ParseException 059 { 060 return parse(options, arguments, null); 061 } 062 063 /** 064 * Parse the arguments according to the specified options and properties. 065 * 066 * @param options the specified Options 067 * @param arguments the command line arguments 068 * @param properties command line option name-value pairs 069 * @return the list of atomic option and value tokens 070 * 071 * @throws ParseException if there are any problems encountered 072 * while parsing the command line tokens. 073 */ 074 public CommandLine parse(Options options, String[] arguments, Properties properties) throws ParseException 075 { 076 return parse(options, arguments, properties, false); 077 } 078 079 public CommandLine parse(Options options, String[] arguments, boolean stopAtNonOption) throws ParseException 080 { 081 return parse(options, arguments, null, stopAtNonOption); 082 } 083 084 /** 085 * Parse the arguments according to the specified options and properties. 086 * 087 * @param options the specified Options 088 * @param arguments the command line arguments 089 * @param properties command line option name-value pairs 090 * @param stopAtNonOption if <tt>true</tt> an unrecognized argument stops 091 * the parsing and the remaining arguments are added to the 092 * {@link CommandLine}s args list. If <tt>false</tt> an unrecognized 093 * argument triggers a ParseException. 094 * 095 * @return the list of atomic option and value tokens 096 * @throws ParseException if there are any problems encountered 097 * while parsing the command line tokens. 098 */ 099 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 if (!token.startsWith("-") || token.length() == 1) 303 { 304 return false; 305 } 306 307 // remove leading "-" and "=value" 308 int pos = token.indexOf("="); 309 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(String token) 324 { 325 if (!token.startsWith("-") || token.length() == 1) 326 { 327 return false; 328 } 329 330 int pos = token.indexOf("="); 331 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(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(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(String token) throws ParseException 403 { 404 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(String token) throws ParseException 430 { 431 int pos = token.indexOf('='); 432 433 String value = token.substring(pos + 1); 434 435 String opt = token.substring(0, pos); 436 437 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 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(String token) throws ParseException 483 { 484 String t = Util.stripLeadingHyphens(token); 485 486 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 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 String opt = t.substring(0, pos); 541 String value = t.substring(pos + 1); 542 543 if (opt.length() == 1) 544 { 545 // -S=V 546 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(String token) 580 { 581 String t = Util.stripLeadingHyphens(token); 582 583 int i; 584 String opt = null; 585 for (i = t.length() - 2; i > 1; i--) 586 { 587 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(String token) 602 { 603 String opt = token.substring(0, 1); 604 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(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 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(String token) throws ParseException 684 { 685 for (int i = 1; i < token.length(); i++) 686 { 687 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}