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 14 * implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 package org.apache.commons.cli.avalon; 20 21 // Renamed from org.apache.avalon.excalibur.cli 22 23 import java.text.ParseException; 24 import java.util.Hashtable; 25 import java.util.Vector; 26 27 /** 28 * Parser for command line arguments. 29 * 30 * This parses command lines according to the standard (?) of GNU utilities. 31 * 32 * Note: This is still used in 1.1 libraries so do not add 1.2+ dependencies. 33 * 34 * Note that CLArgs uses a backing hashtable for the options index and so 35 * duplicate arguments are only returned by getArguments(). 36 * 37 * @see ParserControl 38 * @see CLOption 39 * @see CLOptionDescriptor 40 */ 41 public final class CLArgsParser { 42 // cached character == Integer.MAX_VALUE when invalid 43 private static final int INVALID = Integer.MAX_VALUE; 44 45 private static final int STATE_NORMAL = 0; 46 47 private static final int STATE_REQUIRE_2ARGS = 1; 48 49 private static final int STATE_REQUIRE_ARG = 2; 50 51 private static final int STATE_OPTIONAL_ARG = 3; 52 53 private static final int STATE_NO_OPTIONS = 4; 54 55 private static final int STATE_OPTION_MODE = 5; 56 57 // Values for creating tokens 58 private static final int TOKEN_SEPARATOR = 0; 59 60 private static final int TOKEN_STRING = 1; 61 62 private static final char[] ARG_SEPARATORS = new char[] { (char) 0, '=' }; 63 64 private static final char[] NULL_SEPARATORS = new char[] { (char) 0 }; 65 66 private final CLOptionDescriptor[] m_optionDescriptors; 67 68 private final Vector m_options; 69 70 private Hashtable m_optionIndex; 71 72 private final ParserControl m_control; 73 74 private String m_errorMessage; 75 76 private String[] m_unparsedArgs = new String[] {}; 77 78 // variables used while parsing options. 79 private char m_ch; 80 81 private String[] m_args; 82 83 private boolean m_isLong; 84 85 private int m_argIndex; 86 87 private int m_stringIndex; 88 89 private int m_stringLength; 90 91 private int m_lastChar = INVALID; 92 93 private int m_lastOptionId; 94 95 private CLOption m_option; 96 97 private int m_state = STATE_NORMAL; 98 99 /** 100 * Retrieve an array of arguments that have not been parsed due to the 101 * parser halting. 102 * 103 * @return an array of unparsed args 104 */ 105 public final String[] getUnparsedArgs() { 106 return m_unparsedArgs; 107 } 108 109 /** 110 * Retrieve a list of options that were parsed from command list. 111 * 112 * @return the list of options 113 */ 114 public final Vector getArguments() { 115 // System.out.println( "Arguments: " + m_options ); 116 return m_options; 117 } 118 119 /** 120 * Retrieve the {@link CLOption} with specified id, or <code>null</code> 121 * if no command line option is found. 122 * 123 * @param id 124 * the command line option id 125 * @return the {@link CLOption} with the specified id, or <code>null</code> 126 * if no CLOption is found. 127 * @see CLOption 128 */ 129 public final CLOption getArgumentById(final int id) { 130 return (CLOption) m_optionIndex.get(new Integer(id)); 131 } 132 133 /** 134 * Retrieve the {@link CLOption} with specified name, or <code>null</code> 135 * if no command line option is found. 136 * 137 * @param name 138 * the command line option name 139 * @return the {@link CLOption} with the specified name, or 140 * <code>null</code> if no CLOption is found. 141 * @see CLOption 142 */ 143 public final CLOption getArgumentByName(final String name) { 144 return (CLOption) m_optionIndex.get(name); 145 } 146 147 /** 148 * Get Descriptor for option id. 149 * 150 * @param id 151 * the id 152 * @return the descriptor 153 */ 154 private final CLOptionDescriptor getDescriptorFor(final int id) { 155 for (int i = 0; i < m_optionDescriptors.length; i++) { 156 if (m_optionDescriptors[i].getId() == id) { 157 return m_optionDescriptors[i]; 158 } 159 } 160 161 return null; 162 } 163 164 /** 165 * Retrieve a descriptor by name. 166 * 167 * @param name 168 * the name 169 * @return the descriptor 170 */ 171 private final CLOptionDescriptor getDescriptorFor(final String name) { 172 for (int i = 0; i < m_optionDescriptors.length; i++) { 173 if (m_optionDescriptors[i].getName().equals(name)) { 174 return m_optionDescriptors[i]; 175 } 176 } 177 178 return null; 179 } 180 181 /** 182 * Retrieve an error message that occured during parsing if one existed. 183 * 184 * @return the error string 185 */ 186 public final String getErrorString() { 187 // System.out.println( "ErrorString: " + m_errorMessage ); 188 return m_errorMessage; 189 } 190 191 /** 192 * Require state to be placed in for option. 193 * 194 * @param descriptor 195 * the Option Descriptor 196 * @return the state 197 */ 198 private final int getStateFor(final CLOptionDescriptor descriptor) { 199 final int flags = descriptor.getFlags(); 200 if ((flags & CLOptionDescriptor.ARGUMENTS_REQUIRED_2) == CLOptionDescriptor.ARGUMENTS_REQUIRED_2) { 201 return STATE_REQUIRE_2ARGS; 202 } else if ((flags & CLOptionDescriptor.ARGUMENT_REQUIRED) == CLOptionDescriptor.ARGUMENT_REQUIRED) { 203 return STATE_REQUIRE_ARG; 204 } else if ((flags & CLOptionDescriptor.ARGUMENT_OPTIONAL) == CLOptionDescriptor.ARGUMENT_OPTIONAL) { 205 return STATE_OPTIONAL_ARG; 206 } else { 207 return STATE_NORMAL; 208 } 209 } 210 211 /** 212 * Create a parser that can deal with options and parses certain args. 213 * 214 * @param args 215 * the args, typically that passed to the 216 * <code>public static void main(String[] args)</code> method. 217 * @param optionDescriptors 218 * the option descriptors 219 * @param control 220 * the parser control used determine behaviour of parser 221 */ 222 public CLArgsParser(final String[] args, final CLOptionDescriptor[] optionDescriptors, final ParserControl control) { 223 m_optionDescriptors = optionDescriptors; 224 m_control = control; 225 m_options = new Vector(); 226 m_args = args; 227 228 try { 229 parse(); 230 checkIncompatibilities(m_options); 231 buildOptionIndex(); 232 } catch (final ParseException pe) { 233 m_errorMessage = pe.getMessage(); 234 } 235 236 // System.out.println( "Built : " + m_options ); 237 // System.out.println( "From : " + Arrays.asList( args ) ); 238 } 239 240 /** 241 * Check for duplicates of an option. It is an error to have duplicates 242 * unless appropriate flags is set in descriptor. 243 * 244 * @param arguments 245 * the arguments 246 */ 247 private final void checkIncompatibilities(final Vector arguments) throws ParseException { 248 final int size = arguments.size(); 249 250 for (int i = 0; i < size; i++) { 251 final CLOption option = (CLOption) arguments.elementAt(i); 252 final int id = option.getDescriptor().getId(); 253 final CLOptionDescriptor descriptor = getDescriptorFor(id); 254 255 // this occurs when id == 0 and user has not supplied a descriptor 256 // for arguments 257 if (null == descriptor) { 258 continue; 259 } 260 261 final int[] incompatible = descriptor.getIncompatible(); 262 263 checkIncompatible(arguments, incompatible, i); 264 } 265 } 266 267 private final void checkIncompatible(final Vector arguments, final int[] incompatible, final int original) 268 throws ParseException { 269 final int size = arguments.size(); 270 271 for (int i = 0; i < size; i++) { 272 if (original == i) { 273 continue; 274 } 275 276 final CLOption option = (CLOption) arguments.elementAt(i); 277 final int id = option.getDescriptor().getId(); 278 279 for (int j = 0; j < incompatible.length; j++) { 280 if (id == incompatible[j]) { 281 final CLOption originalOption = (CLOption) arguments.elementAt(original); 282 final int originalId = originalOption.getDescriptor().getId(); 283 284 String message = null; 285 286 if (id == originalId) { 287 message = "Duplicate options for " + describeDualOption(originalId) + " found."; 288 } else { 289 message = "Incompatible options -" + describeDualOption(id) + " and " 290 + describeDualOption(originalId) + " found."; 291 } 292 throw new ParseException(message, 0); 293 } 294 } 295 } 296 } 297 298 private final String describeDualOption(final int id) { 299 final CLOptionDescriptor descriptor = getDescriptorFor(id); 300 if (null == descriptor) { 301 return "<parameter>"; 302 } else { 303 final StringBuffer sb = new StringBuffer(); 304 boolean hasCharOption = false; 305 306 if (Character.isLetter((char) id)) { 307 sb.append('-'); 308 sb.append((char) id); 309 hasCharOption = true; 310 } 311 312 final String longOption = descriptor.getName(); 313 if (null != longOption) { 314 if (hasCharOption) { 315 sb.append('/'); 316 } 317 sb.append("--"); 318 sb.append(longOption); 319 } 320 321 return sb.toString(); 322 } 323 } 324 325 /** 326 * Create a parser that deals with options and parses certain args. 327 * 328 * @param args 329 * the args 330 * @param optionDescriptors 331 * the option descriptors 332 */ 333 public CLArgsParser(final String[] args, final CLOptionDescriptor[] optionDescriptors) { 334 this(args, optionDescriptors, null); 335 } 336 337 /** 338 * Create a string array that is subset of input array. The sub-array should 339 * start at array entry indicated by index. That array element should only 340 * include characters from charIndex onwards. 341 * 342 * @param array 343 * the original array 344 * @param index 345 * the cut-point in array 346 * @param charIndex 347 * the cut-point in element of array 348 * @return the result array 349 */ 350 private final String[] subArray(final String[] array, final int index, final int charIndex) { 351 final int remaining = array.length - index; 352 final String[] result = new String[remaining]; 353 354 if (remaining > 1) { 355 System.arraycopy(array, index + 1, result, 1, remaining - 1); 356 } 357 358 result[0] = array[index].substring(charIndex - 1); 359 360 return result; 361 } 362 363 /** 364 * Actually parse arguments 365 */ 366 private final void parse() throws ParseException { 367 if (0 == m_args.length) { 368 return; 369 } 370 371 m_stringLength = m_args[m_argIndex].length(); 372 373 while (true) { 374 m_ch = peekAtChar(); 375 376 if (m_argIndex >= m_args.length) { 377 break; 378 } 379 380 if (null != m_control && m_control.isFinished(m_lastOptionId)) { 381 // this may need mangling due to peeks 382 m_unparsedArgs = subArray(m_args, m_argIndex, m_stringIndex); 383 return; 384 } 385 386 if (STATE_OPTION_MODE == m_state) { 387 // if get to an arg barrier then return to normal mode 388 // else continue accumulating options 389 if (0 == m_ch) { 390 getChar(); // strip the null 391 m_state = STATE_NORMAL; 392 } else { 393 parseShortOption(); 394 } 395 } else if (STATE_NORMAL == m_state) { 396 parseNormal(); 397 } else if (STATE_NO_OPTIONS == m_state) { 398 // should never get to here when stringIndex != 0 399 addOption(new CLOption(m_args[m_argIndex++])); 400 } else { 401 parseArguments(); 402 } 403 } 404 405 // Reached end of input arguments - perform final processing 406 if (m_option != null) { 407 if (STATE_OPTIONAL_ARG == m_state) { 408 m_options.addElement(m_option); 409 } else if (STATE_REQUIRE_ARG == m_state) { 410 final CLOptionDescriptor descriptor = getDescriptorFor(m_option.getDescriptor().getId()); 411 final String message = "Missing argument to option " + getOptionDescription(descriptor); 412 throw new ParseException(message, 0); 413 } else if (STATE_REQUIRE_2ARGS == m_state) { 414 if (1 == m_option.getArgumentCount()) { 415 m_option.addArgument(""); 416 m_options.addElement(m_option); 417 } else { 418 final CLOptionDescriptor descriptor = getDescriptorFor(m_option.getDescriptor().getId()); 419 final String message = "Missing argument to option " + getOptionDescription(descriptor); 420 throw new ParseException(message, 0); 421 } 422 } else { 423 throw new ParseException("IllegalState " + m_state + ": " + m_option, 0); 424 } 425 } 426 } 427 428 private final String getOptionDescription(final CLOptionDescriptor descriptor) { 429 if (m_isLong) { 430 return "--" + descriptor.getName(); 431 } else { 432 return "-" + (char) descriptor.getId(); 433 } 434 } 435 436 private final char peekAtChar() { 437 if (INVALID == m_lastChar) { 438 m_lastChar = readChar(); 439 } 440 return (char) m_lastChar; 441 } 442 443 private final char getChar() { 444 if (INVALID != m_lastChar) { 445 final char result = (char) m_lastChar; 446 m_lastChar = INVALID; 447 return result; 448 } else { 449 return readChar(); 450 } 451 } 452 453 private final char readChar() { 454 if (m_stringIndex >= m_stringLength) { 455 m_argIndex++; 456 m_stringIndex = 0; 457 458 if (m_argIndex < m_args.length) { 459 m_stringLength = m_args[m_argIndex].length(); 460 } else { 461 m_stringLength = 0; 462 } 463 464 return 0; 465 } 466 467 if (m_argIndex >= m_args.length) { 468 return 0; 469 } 470 471 return m_args[m_argIndex].charAt(m_stringIndex++); 472 } 473 474 private char m_tokesep; // Keep track of token separator 475 476 private final Token nextToken(final char[] separators) { 477 m_ch = getChar(); 478 479 if (isSeparator(m_ch, separators)) { 480 m_tokesep=m_ch; 481 m_ch = getChar(); 482 return new Token(TOKEN_SEPARATOR, null); 483 } 484 485 final StringBuffer sb = new StringBuffer(); 486 487 do { 488 sb.append(m_ch); 489 m_ch = getChar(); 490 } while (!isSeparator(m_ch, separators)); 491 492 m_tokesep=m_ch; 493 return new Token(TOKEN_STRING, sb.toString()); 494 } 495 496 private final boolean isSeparator(final char ch, final char[] separators) { 497 for (int i = 0; i < separators.length; i++) { 498 if (ch == separators[i]) { 499 return true; 500 } 501 } 502 503 return false; 504 } 505 506 private final void addOption(final CLOption option) { 507 m_options.addElement(option); 508 m_lastOptionId = option.getDescriptor().getId(); 509 m_option = null; 510 } 511 512 private final void parseOption(final CLOptionDescriptor descriptor, final String optionString) 513 throws ParseException { 514 if (null == descriptor) { 515 throw new ParseException("Unknown option " + optionString, 0); 516 } 517 518 m_state = getStateFor(descriptor); 519 m_option = new CLOption(descriptor); 520 521 if (STATE_NORMAL == m_state) { 522 addOption(m_option); 523 } 524 } 525 526 private final void parseShortOption() throws ParseException { 527 m_ch = getChar(); 528 final CLOptionDescriptor descriptor = getDescriptorFor(m_ch); 529 m_isLong = false; 530 parseOption(descriptor, "-" + m_ch); 531 532 if (STATE_NORMAL == m_state) { 533 m_state = STATE_OPTION_MODE; 534 } 535 } 536 537 private final void parseArguments() throws ParseException { 538 if (STATE_REQUIRE_ARG == m_state) { 539 if ('=' == m_ch || 0 == m_ch) { 540 getChar(); 541 } 542 543 final Token token = nextToken(NULL_SEPARATORS); 544 m_option.addArgument(token.getValue()); 545 546 addOption(m_option); 547 m_state = STATE_NORMAL; 548 } else if (STATE_OPTIONAL_ARG == m_state) { 549 if ('-' == m_ch || 0 == m_ch) { 550 getChar(); // consume stray character 551 addOption(m_option); 552 m_state = STATE_NORMAL; 553 return; 554 } 555 556 if (m_isLong && '=' != m_tokesep){ // Long optional arg must have = as separator 557 addOption(m_option); 558 m_state = STATE_NORMAL; 559 return; 560 } 561 562 if ('=' == m_ch) { 563 getChar(); 564 } 565 566 final Token token = nextToken(NULL_SEPARATORS); 567 m_option.addArgument(token.getValue()); 568 569 addOption(m_option); 570 m_state = STATE_NORMAL; 571 } else if (STATE_REQUIRE_2ARGS == m_state) { 572 if (0 == m_option.getArgumentCount()) { 573 /* 574 * Fix bug: -D arg1=arg2 was causing parse error; however 575 * --define arg1=arg2 is OK This seems to be because the parser 576 * skips the terminator for the long options, but was not doing 577 * so for the short options. 578 */ 579 if (!m_isLong) { 580 if (0 == peekAtChar()) { 581 getChar(); 582 } 583 } 584 final Token token = nextToken(ARG_SEPARATORS); 585 586 if (TOKEN_SEPARATOR == token.getType()) { 587 final CLOptionDescriptor descriptor = getDescriptorFor(m_option.getDescriptor().getId()); 588 final String message = "Unable to parse first argument for option " 589 + getOptionDescription(descriptor); 590 throw new ParseException(message, 0); 591 } else { 592 m_option.addArgument(token.getValue()); 593 } 594 // Are we about to start a new option? 595 if (0 == m_ch && '-' == peekAtChar()) { 596 // Yes, so the second argument is missing 597 m_option.addArgument(""); 598 m_options.addElement(m_option); 599 m_state = STATE_NORMAL; 600 } 601 } else // 2nd argument 602 { 603 final StringBuffer sb = new StringBuffer(); 604 605 m_ch = getChar(); 606 while (!isSeparator(m_ch, NULL_SEPARATORS)) { 607 sb.append(m_ch); 608 m_ch = getChar(); 609 } 610 611 final String argument = sb.toString(); 612 613 // System.out.println( "Arguement:" + argument ); 614 615 m_option.addArgument(argument); 616 addOption(m_option); 617 m_option = null; 618 m_state = STATE_NORMAL; 619 } 620 } 621 } 622 623 /** 624 * Parse Options from Normal mode. 625 */ 626 private final void parseNormal() throws ParseException { 627 if ('-' != m_ch) { 628 // Parse the arguments that are not options 629 final String argument = nextToken(NULL_SEPARATORS).getValue(); 630 addOption(new CLOption(argument)); 631 m_state = STATE_NORMAL; 632 } else { 633 getChar(); // strip the - 634 635 if (0 == peekAtChar()) { 636 throw new ParseException("Malformed option -", 0); 637 } else { 638 m_ch = peekAtChar(); 639 640 // if it is a short option then parse it else ... 641 if ('-' != m_ch) { 642 parseShortOption(); 643 } else { 644 getChar(); // strip the - 645 // -- sequence .. it can either mean a change of state 646 // to STATE_NO_OPTIONS or else a long option 647 648 if (0 == peekAtChar()) { 649 getChar(); 650 m_state = STATE_NO_OPTIONS; 651 } else { 652 // its a long option 653 final String optionName = nextToken(ARG_SEPARATORS).getValue(); 654 final CLOptionDescriptor descriptor = getDescriptorFor(optionName); 655 m_isLong = true; 656 parseOption(descriptor, "--" + optionName); 657 } 658 } 659 } 660 } 661 } 662 663 /** 664 * Build the m_optionIndex lookup map for the parsed options. 665 */ 666 private final void buildOptionIndex() { 667 final int size = m_options.size(); 668 m_optionIndex = new Hashtable(size * 2); 669 670 for (int i = 0; i < size; i++) { 671 final CLOption option = (CLOption) m_options.get(i); 672 final CLOptionDescriptor optionDescriptor = getDescriptorFor(option.getDescriptor().getId()); 673 674 m_optionIndex.put(new Integer(option.getDescriptor().getId()), option); 675 676 if (null != optionDescriptor && null != optionDescriptor.getName()) { 677 m_optionIndex.put(optionDescriptor.getName(), option); 678 } 679 } 680 } 681 }