1 /** 2 * Copyright (C) 2010 the original author or authors. 3 * See the notice.md file distributed with this work for additional 4 * information regarding copyright ownership. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package com.beust.jcommander; 20 21 import com.beust.jcommander.FuzzyMap.IKey; 22 import com.beust.jcommander.converters.*; 23 import com.beust.jcommander.internal.*; 24 25 import java.io.BufferedReader; 26 import java.io.IOException; 27 import java.lang.reflect.*; 28 import java.nio.charset.Charset; 29 import java.nio.file.Files; 30 import java.nio.file.Paths; 31 import java.util.*; 32 import java.util.ResourceBundle; 33 import java.util.concurrent.CopyOnWriteArrayList; 34 35 /** 36 * The main class for JCommander. It's responsible for parsing the object that contains 37 * all the annotated fields, parse the command line and assign the fields with the correct 38 * values and a few other helper methods, such as usage(). 39 * 40 * The object(s) you pass in the constructor are expected to have one or more 41 * \@Parameter annotations on them. You can pass either a single object, an array of objects 42 * or an instance of Iterable. In the case of an array or Iterable, JCommander will collect 43 * the \@Parameter annotations from all the objects passed in parameter. 44 * 45 * @author Cedric Beust <cedric@beust.com> 46 */ 47 public class JCommander { 48 public static final String DEBUG_PROPERTY = "jcommander.debug"; 49 50 /** 51 * A map to look up parameter description per option name. 52 */ 53 private Map<IKey, ParameterDescription> descriptions; 54 55 /** 56 * The objects that contain fields annotated with @Parameter. 57 */ 58 private List<Object> objects = Lists.newArrayList(); 59 60 private boolean firstTimeMainParameter = true; 61 62 /** 63 * This field/method will contain whatever command line parameter is not an option. 64 * It is expected to be a List<String>. 65 */ 66 private Parameterized mainParameter = null; 67 68 /** 69 * The object on which we found the main parameter field. 70 */ 71 private Object mainParameterObject; 72 73 /** 74 * The annotation found on the main parameter field. 75 */ 76 private Parameter mainParameterAnnotation; 77 78 private ParameterDescription mainParameterDescription; 79 80 /** 81 * A set of all the parameterizeds that are required. During the reflection phase, 82 * this field receives all the fields that are annotated with required=true 83 * and during the parsing phase, all the fields that are assigned a value 84 * are removed from it. At the end of the parsing phase, if it's not empty, 85 * then some required fields did not receive a value and an exception is 86 * thrown. 87 */ 88 private Map<Parameterized, ParameterDescription> requiredFields = Maps.newHashMap(); 89 90 /** 91 * A map of all the parameterized fields/methods. 92 */ 93 private Map<Parameterized, ParameterDescription> fields = Maps.newHashMap(); 94 95 /** 96 * List of commands and their instance. 97 */ 98 private Map<ProgramName, JCommander> commands = Maps.newLinkedHashMap(); 99 100 /** 101 * Alias database for reverse lookup 102 */ 103 private Map<IKey, ProgramName> aliasMap = Maps.newLinkedHashMap(); 104 105 /** 106 * The name of the command after the parsing has run. 107 */ 108 private String parsedCommand; 109 110 /** 111 * The name of command or alias as it was passed to the 112 * command line 113 */ 114 private String parsedAlias; 115 116 private ProgramName programName; 117 118 private boolean helpWasSpecified; 119 120 private List<String> unknownArgs = Lists.newArrayList(); 121 122 private static Console console; 123 124 private final Options options; 125 126 /** 127 * Options shared with sub commands 128 */ 129 private static class Options { 130 131 private ResourceBundle bundle; 132 133 /** 134 * A default provider returns default values for the parameters. 135 */ 136 private IDefaultProvider defaultProvider; 137 138 private Comparator<? super ParameterDescription> parameterDescriptionComparator 139 = new Comparator<ParameterDescription>() { 140 @Override 141 public int compare(ParameterDescription p0, ParameterDescription p1) { 142 Parameter a0 = p0.getParameterAnnotation(); 143 Parameter a1 = p1.getParameterAnnotation(); 144 if (a0 != null && a0.order() != -1 && a1 != null && a1.order() != -1) { 145 return Integer.compare(a0.order(), a1.order()); 146 } else if (a0 != null && a0.order() != -1) { 147 return -1; 148 } else if (a1 != null && a1.order() != -1) { 149 return 1; 150 } else { 151 return p0.getLongestName().compareTo(p1.getLongestName()); 152 } 153 } 154 }; 155 private int columnSize = 79; 156 private boolean acceptUnknownOptions = false; 157 private boolean allowParameterOverwriting = false; 158 private boolean expandAtSign = true; 159 private int verbose = 0; 160 private boolean caseSensitiveOptions = true; 161 private boolean allowAbbreviatedOptions = false; 162 /** 163 * The factories used to look up string converters. 164 */ 165 private final List<IStringConverterInstanceFactory> converterInstanceFactories = new CopyOnWriteArrayList<>(); 166 private Charset atFileCharset = Charset.defaultCharset(); 167 } 168 JCommander(Options options)169 private JCommander(Options options) { 170 if (options == null) { 171 throw new NullPointerException("options"); 172 } 173 this.options = options; 174 addConverterFactory(new DefaultConverterFactory()); 175 } 176 177 /** 178 * Creates a new un-configured JCommander object. 179 */ JCommander()180 public JCommander() { 181 this(new Options()); 182 } 183 184 /** 185 * @param object The arg object expected to contain {@link Parameter} annotations. 186 */ JCommander(Object object)187 public JCommander(Object object) { 188 this(object, (ResourceBundle) null); 189 } 190 191 /** 192 * @param object The arg object expected to contain {@link Parameter} annotations. 193 * @param bundle The bundle to use for the descriptions. Can be null. 194 */ JCommander(Object object, @Nullable ResourceBundle bundle)195 public JCommander(Object object, @Nullable ResourceBundle bundle) { 196 this(object, bundle, (String[]) null); 197 } 198 199 /** 200 * @param object The arg object expected to contain {@link Parameter} annotations. 201 * @param bundle The bundle to use for the descriptions. Can be null. 202 * @param args The arguments to parse (optional). 203 */ JCommander(Object object, @Nullable ResourceBundle bundle, String... args)204 public JCommander(Object object, @Nullable ResourceBundle bundle, String... args) { 205 this(); 206 addObject(object); 207 if (bundle != null) { 208 setDescriptionsBundle(bundle); 209 } 210 createDescriptions(); 211 if (args != null) { 212 parse(args); 213 } 214 } 215 216 /** 217 * @param object The arg object expected to contain {@link Parameter} annotations. 218 * @param args The arguments to parse (optional). 219 * 220 * @deprecated Construct a JCommander instance first and then call parse() on it. 221 */ 222 @Deprecated() JCommander(Object object, String... args)223 public JCommander(Object object, String... args) { 224 this(object); 225 parse(args); 226 } 227 228 /** 229 * Disables expanding {@code @file}. 230 * 231 * JCommander supports the {@code @file} syntax, which allows you to put all your options 232 * into a file and pass this file as parameter @param expandAtSign whether to expand {@code @file}. 233 */ setExpandAtSign(boolean expandAtSign)234 public void setExpandAtSign(boolean expandAtSign) { 235 options.expandAtSign = expandAtSign; 236 } 237 getConsole()238 public static Console getConsole() { 239 if (console == null) { 240 try { 241 Method consoleMethod = System.class.getDeclaredMethod("console"); 242 Object console = consoleMethod.invoke(null); 243 JCommander.console = new JDK6Console(console); 244 } catch (Throwable t) { 245 console = new DefaultConsole(); 246 } 247 } 248 return console; 249 } 250 251 /** 252 * Adds the provided arg object to the set of objects that this commander 253 * will parse arguments into. 254 * 255 * @param object The arg object expected to contain {@link Parameter} 256 * annotations. If <code>object</code> is an array or is {@link Iterable}, 257 * the child objects will be added instead. 258 */ 259 // declared final since this is invoked from constructors addObject(Object object)260 public final void addObject(Object object) { 261 if (object instanceof Iterable) { 262 // Iterable 263 for (Object o : (Iterable<?>) object) { 264 objects.add(o); 265 } 266 } else if (object.getClass().isArray()) { 267 // Array 268 for (Object o : (Object[]) object) { 269 objects.add(o); 270 } 271 } else { 272 // Single object 273 objects.add(object); 274 } 275 } 276 277 /** 278 * Sets the {@link ResourceBundle} to use for looking up descriptions. 279 * Set this to <code>null</code> to use description text directly. 280 */ 281 // declared final since this is invoked from constructors setDescriptionsBundle(ResourceBundle bundle)282 public final void setDescriptionsBundle(ResourceBundle bundle) { 283 options.bundle = bundle; 284 } 285 286 /** 287 * Parse and validate the command line parameters. 288 */ parse(String... args)289 public void parse(String... args) { 290 try { 291 parse(true /* validate */, args); 292 } catch(ParameterException ex) { 293 ex.setJCommander(this); 294 throw ex; 295 } 296 } 297 298 /** 299 * Parse the command line parameters without validating them. 300 */ parseWithoutValidation(String... args)301 public void parseWithoutValidation(String... args) { 302 parse(false /* no validation */, args); 303 } 304 parse(boolean validate, String... args)305 private void parse(boolean validate, String... args) { 306 StringBuilder sb = new StringBuilder("Parsing \""); 307 sb.append(join(args).append("\"\n with:").append(join(objects.toArray()))); 308 p(sb.toString()); 309 310 if (descriptions == null) createDescriptions(); 311 initializeDefaultValues(); 312 parseValues(expandArgs(args), validate); 313 if (validate) validateOptions(); 314 } 315 join(Object[] args)316 private StringBuilder join(Object[] args) { 317 StringBuilder result = new StringBuilder(); 318 for (int i = 0; i < args.length; i++) { 319 if (i > 0) result.append(" "); 320 result.append(args[i]); 321 } 322 return result; 323 } 324 initializeDefaultValues()325 private void initializeDefaultValues() { 326 if (options.defaultProvider != null) { 327 for (ParameterDescription pd : descriptions.values()) { 328 initializeDefaultValue(pd); 329 } 330 331 for (Map.Entry<ProgramName, JCommander> entry : commands.entrySet()) { 332 entry.getValue().initializeDefaultValues(); 333 } 334 } 335 } 336 337 /** 338 * Make sure that all the required parameters have received a value. 339 */ validateOptions()340 private void validateOptions() { 341 // No validation if we found a help parameter 342 if (helpWasSpecified) { 343 return; 344 } 345 346 if (!requiredFields.isEmpty()) { 347 List<String> missingFields = new ArrayList<>(); 348 for (ParameterDescription pd : requiredFields.values()) { 349 missingFields.add("[" + String.join(" | ", pd.getParameter().names()) + "]"); 350 } 351 String message = String.join(", ", missingFields); 352 throw new ParameterException("The following " 353 + pluralize(requiredFields.size(), "option is required: ", "options are required: ") 354 + message); 355 } 356 357 if (mainParameterDescription != null) { 358 if (mainParameterDescription.getParameter().required() && 359 !mainParameterDescription.isAssigned()) { 360 throw new ParameterException("Main parameters are required (\"" 361 + mainParameterDescription.getDescription() + "\")"); 362 } 363 } 364 } 365 pluralize(int quantity, String singular, String plural)366 private static String pluralize(int quantity, String singular, String plural) { 367 return quantity == 1 ? singular : plural; 368 } 369 370 /** 371 * Expand the command line parameters to take @ parameters into account. 372 * When @ is encountered, the content of the file that follows is inserted 373 * in the command line. 374 * 375 * @param originalArgv the original command line parameters 376 * @return the new and enriched command line parameters 377 */ expandArgs(String[] originalArgv)378 private String[] expandArgs(String[] originalArgv) { 379 List<String> vResult1 = Lists.newArrayList(); 380 381 // 382 // Expand @ 383 // 384 for (String arg : originalArgv) { 385 386 if (arg.startsWith("@") && options.expandAtSign) { 387 String fileName = arg.substring(1); 388 vResult1.addAll(readFile(fileName)); 389 } else { 390 List<String> expanded = expandDynamicArg(arg); 391 vResult1.addAll(expanded); 392 } 393 } 394 395 // Expand separators 396 // 397 List<String> vResult2 = Lists.newArrayList(); 398 for (String arg : vResult1) { 399 if (isOption(arg)) { 400 String sep = getSeparatorFor(arg); 401 if (!" ".equals(sep)) { 402 String[] sp = arg.split("[" + sep + "]", 2); 403 for (String ssp : sp) { 404 vResult2.add(ssp); 405 } 406 } else { 407 vResult2.add(arg); 408 } 409 } else { 410 vResult2.add(arg); 411 } 412 } 413 414 return vResult2.toArray(new String[vResult2.size()]); 415 } 416 expandDynamicArg(String arg)417 private List<String> expandDynamicArg(String arg) { 418 for (ParameterDescription pd : descriptions.values()) { 419 if (pd.isDynamicParameter()) { 420 for (String name : pd.getParameter().names()) { 421 if (arg.startsWith(name) && !arg.equals(name)) { 422 return Arrays.asList(name, arg.substring(name.length())); 423 } 424 } 425 } 426 } 427 428 return Arrays.asList(arg); 429 } 430 matchArg(String arg, IKey key)431 private boolean matchArg(String arg, IKey key) { 432 String kn = options.caseSensitiveOptions 433 ? key.getName() 434 : key.getName().toLowerCase(); 435 if (options.allowAbbreviatedOptions) { 436 if (kn.startsWith(arg)) return true; 437 } else { 438 ParameterDescription pd = descriptions.get(key); 439 if (pd != null) { 440 // It's an option. If the option has a separator (e.g. -author==foo) then 441 // we only do a beginsWith match 442 String separator = getSeparatorFor(arg); 443 if (! " ".equals(separator)) { 444 if (arg.startsWith(kn)) return true; 445 } else { 446 if (kn.equals(arg)) return true; 447 } 448 } else { 449 // It's a command do a strict equality check 450 if (kn.equals(arg)) return true; 451 } 452 } 453 return false; 454 } 455 isOption(String passedArg)456 private boolean isOption(String passedArg) { 457 if (options.acceptUnknownOptions) return true; 458 459 String arg = options.caseSensitiveOptions ? passedArg : passedArg.toLowerCase(); 460 461 for (IKey key : descriptions.keySet()) { 462 if (matchArg(arg, key)) return true; 463 } 464 for (IKey key : commands.keySet()) { 465 if (matchArg(arg, key)) return true; 466 } 467 468 return false; 469 } 470 getPrefixDescriptionFor(String arg)471 private ParameterDescription getPrefixDescriptionFor(String arg) { 472 for (Map.Entry<IKey, ParameterDescription> es : descriptions.entrySet()) { 473 if (arg.startsWith(es.getKey().getName())) return es.getValue(); 474 } 475 476 return null; 477 } 478 479 /** 480 * If arg is an option, we can look it up directly, but if it's a value, 481 * we need to find the description for the option that precedes it. 482 */ getDescriptionFor(String arg)483 private ParameterDescription getDescriptionFor(String arg) { 484 return getPrefixDescriptionFor(arg); 485 } 486 getSeparatorFor(String arg)487 private String getSeparatorFor(String arg) { 488 ParameterDescription pd = getDescriptionFor(arg); 489 490 // Could be null if only main parameters were passed 491 if (pd != null) { 492 Parameters p = pd.getObject().getClass().getAnnotation(Parameters.class); 493 if (p != null) return p.separators(); 494 } 495 496 return " "; 497 } 498 499 /** 500 * Reads the file specified by filename and returns the file content as a string. 501 * End of lines are replaced by a space. 502 * 503 * @param fileName the command line filename 504 * @return the file content as a string. 505 */ readFile(String fileName)506 private List<String> readFile(String fileName) { 507 List<String> result = Lists.newArrayList(); 508 509 try (BufferedReader bufRead = Files.newBufferedReader(Paths.get(fileName), options.atFileCharset)) { 510 String line; 511 // Read through file one line at time. Print line # and line 512 while ((line = bufRead.readLine()) != null) { 513 // Allow empty lines and # comments in these at files 514 if (line.length() > 0 && !line.trim().startsWith("#")) { 515 result.add(line); 516 } 517 } 518 } catch (IOException e) { 519 throw new ParameterException("Could not read file " + fileName + ": " + e); 520 } 521 522 return result; 523 } 524 525 /** 526 * Remove spaces at both ends and handle double quotes. 527 */ trim(String string)528 private static String trim(String string) { 529 String result = string.trim(); 530 if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 1) { 531 result = result.substring(1, result.length() - 1); 532 } 533 return result; 534 } 535 536 /** 537 * Create the ParameterDescriptions for all the \@Parameter found. 538 */ createDescriptions()539 private void createDescriptions() { 540 descriptions = Maps.newHashMap(); 541 542 for (Object object : objects) { 543 addDescription(object); 544 } 545 } 546 addDescription(Object object)547 private void addDescription(Object object) { 548 Class<?> cls = object.getClass(); 549 550 List<Parameterized> parameterizeds = Parameterized.parseArg(object); 551 for (Parameterized parameterized : parameterizeds) { 552 WrappedParameter wp = parameterized.getWrappedParameter(); 553 if (wp != null && wp.getParameter() != null) { 554 Parameter annotation = wp.getParameter(); 555 // 556 // @Parameter 557 // 558 Parameter p = annotation; 559 if (p.names().length == 0) { 560 p("Found main parameter:" + parameterized); 561 if (mainParameter != null) { 562 throw new ParameterException("Only one @Parameter with no names attribute is" 563 + " allowed, found:" + mainParameter + " and " + parameterized); 564 } 565 mainParameter = parameterized; 566 mainParameterObject = object; 567 mainParameterAnnotation = p; 568 mainParameterDescription = 569 new ParameterDescription(object, p, parameterized, options.bundle, this); 570 } else { 571 ParameterDescription pd = 572 new ParameterDescription(object, p, parameterized, options.bundle, this); 573 for (String name : p.names()) { 574 if (descriptions.containsKey(new StringKey(name))) { 575 throw new ParameterException("Found the option " + name + " multiple times"); 576 } 577 p("Adding description for " + name); 578 fields.put(parameterized, pd); 579 descriptions.put(new StringKey(name), pd); 580 581 if (p.required()) requiredFields.put(parameterized, pd); 582 } 583 } 584 } else if (parameterized.getDelegateAnnotation() != null) { 585 // 586 // @ParametersDelegate 587 // 588 Object delegateObject = parameterized.get(object); 589 if (delegateObject == null) { 590 throw new ParameterException("Delegate field '" + parameterized.getName() 591 + "' cannot be null."); 592 } 593 addDescription(delegateObject); 594 } else if (wp != null && wp.getDynamicParameter() != null) { 595 // 596 // @DynamicParameter 597 // 598 DynamicParameter dp = wp.getDynamicParameter(); 599 for (String name : dp.names()) { 600 if (descriptions.containsKey(name)) { 601 throw new ParameterException("Found the option " + name + " multiple times"); 602 } 603 p("Adding description for " + name); 604 ParameterDescription pd = 605 new ParameterDescription(object, dp, parameterized, options.bundle, this); 606 fields.put(parameterized, pd); 607 descriptions.put(new StringKey(name), pd); 608 609 if (dp.required()) requiredFields.put(parameterized, pd); 610 } 611 } 612 } 613 } 614 initializeDefaultValue(ParameterDescription pd)615 private void initializeDefaultValue(ParameterDescription pd) { 616 for (String optionName : pd.getParameter().names()) { 617 String def = options.defaultProvider.getDefaultValueFor(optionName); 618 if (def != null) { 619 p("Initializing " + optionName + " with default value:" + def); 620 pd.addValue(def, true /* default */); 621 // remove the parameter from the list of fields to be required 622 requiredFields.remove(pd.getParameterized()); 623 return; 624 } 625 } 626 } 627 628 /** 629 * Main method that parses the values and initializes the fields accordingly. 630 */ parseValues(String[] args, boolean validate)631 private void parseValues(String[] args, boolean validate) { 632 // This boolean becomes true if we encounter a command, which indicates we need 633 // to stop parsing (the parsing of the command will be done in a sub JCommander 634 // object) 635 boolean commandParsed = false; 636 int i = 0; 637 boolean isDashDash = false; // once we encounter --, everything goes into the main parameter 638 while (i < args.length && !commandParsed) { 639 String arg = args[i]; 640 String a = trim(arg); 641 args[i] = a; 642 p("Parsing arg: " + a); 643 644 JCommander jc = findCommandByAlias(arg); 645 int increment = 1; 646 if (!isDashDash && !"--".equals(a) && isOption(a) && jc == null) { 647 // 648 // Option 649 // 650 ParameterDescription pd = findParameterDescription(a); 651 652 if (pd != null) { 653 if (pd.getParameter().password()) { 654 increment = processPassword(args, i, pd, validate); 655 } else { 656 if (pd.getParameter().variableArity()) { 657 // 658 // Variable arity? 659 // 660 increment = processVariableArity(args, i, pd, validate); 661 } else { 662 // 663 // Regular option 664 // 665 Class<?> fieldType = pd.getParameterized().getType(); 666 667 // Boolean, set to true as soon as we see it, unless it specified 668 // an arity of 1, in which case we need to read the next value 669 if ((fieldType == boolean.class || fieldType == Boolean.class) 670 && pd.getParameter().arity() == -1) { 671 // Flip the value this boolean was initialized with 672 Boolean value = (Boolean) pd.getParameterized().get(pd.getObject()); 673 pd.addValue(value ? "false" : "true"); 674 requiredFields.remove(pd.getParameterized()); 675 } else { 676 increment = processFixedArity(args, i, pd, validate, fieldType); 677 } 678 // If it's a help option, remember for later 679 if (pd.isHelp()) { 680 helpWasSpecified = true; 681 } 682 } 683 } 684 } else { 685 if (options.acceptUnknownOptions) { 686 unknownArgs.add(arg); 687 i++; 688 while (i < args.length && !isOption(args[i])) { 689 unknownArgs.add(args[i++]); 690 } 691 increment = 0; 692 } else { 693 throw new ParameterException("Unknown option: " + arg); 694 } 695 } 696 } else { 697 // 698 // Main parameter 699 // 700 if ("--".equals(arg) && !isDashDash) { 701 isDashDash = true; 702 } 703 else if (commands.isEmpty()) { 704 // 705 // Regular (non-command) parsing 706 // 707 List mp = getMainParameter(arg); 708 String value = a; // If there's a non-quoted version, prefer that one 709 Object convertedValue = value; 710 711 if (mainParameter.getGenericType() instanceof ParameterizedType) { 712 ParameterizedType p = (ParameterizedType) mainParameter.getGenericType(); 713 Type cls = p.getActualTypeArguments()[0]; 714 if (cls instanceof Class) { 715 convertedValue = convertValue(mainParameter, (Class) cls, null, value); 716 } 717 } 718 719 for(final Class<? extends IParameterValidator> validator : mainParameterAnnotation.validateWith() ) { 720 ParameterDescription.validateParameter(mainParameterDescription, 721 validator, 722 "Default", value); 723 } 724 725 mainParameterDescription.setAssigned(true); 726 mp.add(convertedValue); 727 } else { 728 // 729 // Command parsing 730 // 731 if (jc == null && validate) { 732 throw new MissingCommandException("Expected a command, got " + arg, arg); 733 } else if (jc != null) { 734 parsedCommand = jc.programName.name; 735 parsedAlias = arg; //preserve the original form 736 737 // Found a valid command, ask it to parse the remainder of the arguments. 738 // Setting the boolean commandParsed to true will force the current 739 // loop to end. 740 jc.parse(validate, subArray(args, i + 1)); 741 commandParsed = true; 742 } 743 } 744 } 745 i += increment; 746 } 747 748 // Mark the parameter descriptions held in fields as assigned 749 for (ParameterDescription parameterDescription : descriptions.values()) { 750 if (parameterDescription.isAssigned()) { 751 fields.get(parameterDescription.getParameterized()).setAssigned(true); 752 } 753 } 754 755 } 756 757 private class DefaultVariableArity implements IVariableArity { 758 759 @Override processVariableArity(String optionName, String[] options)760 public int processVariableArity(String optionName, String[] options) { 761 int i = 0; 762 while (i < options.length && !isOption(options[i])) { 763 i++; 764 } 765 return i; 766 } 767 } 768 769 private final IVariableArity DEFAULT_VARIABLE_ARITY = new DefaultVariableArity(); 770 determineArity(String[] args, int index, ParameterDescription pd, IVariableArity va)771 private final int determineArity(String[] args, int index, ParameterDescription pd, IVariableArity va) { 772 List<String> currentArgs = Lists.newArrayList(); 773 for (int j = index + 1; j < args.length; j++) { 774 currentArgs.add(args[j]); 775 } 776 return va.processVariableArity(pd.getParameter().names()[0], 777 currentArgs.toArray(new String[0])); 778 } 779 780 /** 781 * @return the number of options that were processed. 782 */ processPassword(String[] args, int index, ParameterDescription pd, boolean validate)783 private int processPassword(String[] args, int index, ParameterDescription pd, boolean validate) { 784 final int passwordArity = determineArity(args, index, pd, DEFAULT_VARIABLE_ARITY); 785 if (passwordArity == 0) { 786 // password option with password not specified, use the Console to retrieve the password 787 char[] password = readPassword(pd.getDescription(), pd.getParameter().echoInput()); 788 pd.addValue(new String(password)); 789 requiredFields.remove(pd.getParameterized()); 790 return 1; 791 } else if (passwordArity == 1) { 792 // password option with password specified 793 return processFixedArity(args, index, pd, validate, List.class, 1); 794 } else { 795 throw new ParameterException("Password parameter must have at most 1 argument."); 796 } 797 } 798 799 /** 800 * @return the number of options that were processed. 801 */ processVariableArity(String[] args, int index, ParameterDescription pd, boolean validate)802 private int processVariableArity(String[] args, int index, ParameterDescription pd, boolean validate) { 803 Object arg = pd.getObject(); 804 IVariableArity va; 805 if (!(arg instanceof IVariableArity)) { 806 va = DEFAULT_VARIABLE_ARITY; 807 } else { 808 va = (IVariableArity) arg; 809 } 810 811 int arity = determineArity(args, index, pd, va); 812 int result = processFixedArity(args, index, pd, validate, List.class, arity); 813 return result; 814 } 815 processFixedArity(String[] args, int index, ParameterDescription pd, boolean validate, Class<?> fieldType)816 private int processFixedArity(String[] args, int index, ParameterDescription pd, boolean validate, 817 Class<?> fieldType) { 818 // Regular parameter, use the arity to tell use how many values 819 // we need to consume 820 int arity = pd.getParameter().arity(); 821 int n = (arity != -1 ? arity : 1); 822 823 return processFixedArity(args, index, pd, validate, fieldType, n); 824 } 825 processFixedArity(String[] args, int originalIndex, ParameterDescription pd, boolean validate, Class<?> fieldType, int arity)826 private int processFixedArity(String[] args, int originalIndex, ParameterDescription pd, boolean validate, 827 Class<?> fieldType, int arity) { 828 int index = originalIndex; 829 String arg = args[index]; 830 // Special case for boolean parameters of arity 0 831 if (arity == 0 && 832 (Boolean.class.isAssignableFrom(fieldType) 833 || boolean.class.isAssignableFrom(fieldType))) { 834 // Flip the value this boolean was initialized with 835 Boolean value = (Boolean) pd.getParameterized().get(pd.getObject()); 836 pd.addValue(value ? "false" : "true"); 837 requiredFields.remove(pd.getParameterized()); 838 } else if (arity == 0) { 839 throw new ParameterException("Expected a value after parameter " + arg); 840 841 } else if (index < args.length - 1) { 842 int offset = "--".equals(args[index + 1]) ? 1 : 0; 843 844 Object finalValue = null; 845 if (index + arity < args.length) { 846 for (int j = 1; j <= arity; j++) { 847 String value = trim(args[index + j + offset]); 848 finalValue = pd.addValue(arg, value, false, validate, j - 1); 849 requiredFields.remove(pd.getParameterized()); 850 } 851 852 if (finalValue != null && validate) { 853 pd.validateValueParameter(arg, finalValue); 854 } 855 index += arity + offset; 856 } else { 857 throw new ParameterException("Expected " + arity + " values after " + arg); 858 } 859 } else { 860 throw new ParameterException("Expected a value after parameter " + arg); 861 } 862 863 return arity + 1; 864 } 865 866 /** 867 * Invoke Console.readPassword through reflection to avoid depending 868 * on Java 6. 869 */ readPassword(String description, boolean echoInput)870 private char[] readPassword(String description, boolean echoInput) { 871 getConsole().print(description + ": "); 872 return getConsole().readPassword(echoInput); 873 } 874 subArray(String[] args, int index)875 private String[] subArray(String[] args, int index) { 876 int l = args.length - index; 877 String[] result = new String[l]; 878 System.arraycopy(args, index, result, 0, l); 879 880 return result; 881 } 882 883 /** 884 * @return the field that's meant to receive all the parameters that are not options. 885 * 886 * @param arg the arg that we're about to add (only passed here to output a meaningful 887 * error message). 888 */ getMainParameter(String arg)889 private List<?> getMainParameter(String arg) { 890 if (mainParameter == null) { 891 throw new ParameterException( 892 "Was passed main parameter '" + arg + "' but no main parameter was defined in your arg class"); 893 } 894 895 List<?> result = (List<?>) mainParameter.get(mainParameterObject); 896 if (result == null) { 897 result = Lists.newArrayList(); 898 if (!List.class.isAssignableFrom(mainParameter.getType())) { 899 throw new ParameterException("Main parameter field " + mainParameter 900 + " needs to be of type List, not " + mainParameter.getType()); 901 } 902 mainParameter.set(mainParameterObject, result); 903 } 904 if (firstTimeMainParameter) { 905 result.clear(); 906 firstTimeMainParameter = false; 907 } 908 return result; 909 } 910 getMainParameterDescription()911 public String getMainParameterDescription() { 912 if (descriptions == null) createDescriptions(); 913 return mainParameterAnnotation != null ? mainParameterAnnotation.description() 914 : null; 915 } 916 917 /** 918 * Set the program name (used only in the usage). 919 */ setProgramName(String name)920 public void setProgramName(String name) { 921 setProgramName(name, new String[0]); 922 } 923 924 /** 925 * Get the program name (used only in the usage). 926 */ getProgramName()927 public String getProgramName(){ 928 return programName == null ? null : programName.getName(); 929 } 930 931 /** 932 * Set the program name 933 * 934 * @param name program name 935 * @param aliases aliases to the program name 936 */ setProgramName(String name, String... aliases)937 public void setProgramName(String name, String... aliases) { 938 programName = new ProgramName(name, Arrays.asList(aliases)); 939 } 940 941 /** 942 * Display the usage for this command. 943 */ usage(String commandName)944 public void usage(String commandName) { 945 StringBuilder sb = new StringBuilder(); 946 usage(commandName, sb); 947 getConsole().println(sb.toString()); 948 } 949 950 /** 951 * Store the help for the command in the passed string builder. 952 */ usage(String commandName, StringBuilder out)953 public void usage(String commandName, StringBuilder out) { 954 usage(commandName, out, ""); 955 } 956 957 /** 958 * Store the help for the command in the passed string builder, indenting 959 * every line with "indent". 960 */ usage(String commandName, StringBuilder out, String indent)961 public void usage(String commandName, StringBuilder out, String indent) { 962 String description = getCommandDescription(commandName); 963 JCommander jc = findCommandByAlias(commandName); 964 if (description != null) { 965 out.append(indent).append(description); 966 out.append("\n"); 967 } 968 jc.usage(out, indent); 969 } 970 971 /** 972 * @return the description of the command. 973 */ getCommandDescription(String commandName)974 public String getCommandDescription(String commandName) { 975 JCommander jc = findCommandByAlias(commandName); 976 if (jc == null) { 977 throw new ParameterException("Asking description for unknown command: " + commandName); 978 } 979 980 Object arg = jc.getObjects().get(0); 981 Parameters p = arg.getClass().getAnnotation(Parameters.class); 982 ResourceBundle bundle = null; 983 String result = null; 984 if (p != null) { 985 result = p.commandDescription(); 986 String bundleName = p.resourceBundle(); 987 if (!"".equals(bundleName)) { 988 bundle = ResourceBundle.getBundle(bundleName, Locale.getDefault()); 989 } else { 990 bundle = options.bundle; 991 } 992 993 if (bundle != null) { 994 String descriptionKey = p.commandDescriptionKey(); 995 if (!"".equals(descriptionKey)) { 996 result = getI18nString(bundle, descriptionKey, p.commandDescription()); 997 } 998 } 999 } 1000 1001 return result; 1002 } 1003 1004 /** 1005 * @return The internationalized version of the string if available, otherwise 1006 * return def. 1007 */ getI18nString(ResourceBundle bundle, String key, String def)1008 private String getI18nString(ResourceBundle bundle, String key, String def) { 1009 String s = bundle != null ? bundle.getString(key) : null; 1010 return s != null ? s : def; 1011 } 1012 1013 /** 1014 * Display the help on System.out. 1015 */ usage()1016 public void usage() { 1017 StringBuilder sb = new StringBuilder(); 1018 usage(sb); 1019 getConsole().println(sb.toString()); 1020 } 1021 newBuilder()1022 public static Builder newBuilder() { 1023 return new Builder(); 1024 } 1025 1026 public static class Builder { 1027 private JCommander jCommander = new JCommander(); 1028 private String[] args = null; 1029 Builder()1030 public Builder() { 1031 } 1032 1033 /** 1034 * Adds the provided arg object to the set of objects that this commander 1035 * will parse arguments into. 1036 * 1037 * @param o The arg object expected to contain {@link Parameter} 1038 * annotations. If <code>object</code> is an array or is {@link Iterable}, 1039 * the child objects will be added instead. 1040 */ addObject(Object o)1041 public Builder addObject(Object o) { 1042 jCommander.addObject(o); 1043 return this; 1044 } 1045 1046 /** 1047 * Sets the {@link ResourceBundle} to use for looking up descriptions. 1048 * Set this to <code>null</code> to use description text directly. 1049 */ resourceBundle(ResourceBundle bundle)1050 public Builder resourceBundle(ResourceBundle bundle) { 1051 jCommander.setDescriptionsBundle(bundle); 1052 return this; 1053 } 1054 args(String[] args)1055 public Builder args(String[] args) { 1056 this.args = args; 1057 return this; 1058 } 1059 1060 /** 1061 * Disables expanding {@code @file}. 1062 * 1063 * JCommander supports the {@code @file} syntax, which allows you to put all your options 1064 * into a file and pass this file as parameter @param expandAtSign whether to expand {@code @file}. 1065 */ expandAtSign(Boolean expand)1066 public Builder expandAtSign(Boolean expand) { 1067 jCommander.setExpandAtSign(expand); 1068 return this; 1069 } 1070 1071 /** 1072 * Set the program name (used only in the usage). 1073 */ programName(String name)1074 public Builder programName(String name) { 1075 jCommander.setProgramName(name); 1076 return this; 1077 } 1078 columnSize(int columnSize)1079 public Builder columnSize(int columnSize) { 1080 jCommander.setColumnSize(columnSize); 1081 return this; 1082 } 1083 1084 /** 1085 * Define the default provider for this instance. 1086 */ defaultProvider(IDefaultProvider provider)1087 public Builder defaultProvider(IDefaultProvider provider) { 1088 jCommander.setDefaultProvider(provider); 1089 return this; 1090 } 1091 1092 /** 1093 * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. 1094 * @param factory the factory determining string converters 1095 */ addConverterFactory(IStringConverterFactory factory)1096 public Builder addConverterFactory(IStringConverterFactory factory) { 1097 jCommander.addConverterFactory(factory); 1098 return this; 1099 } 1100 verbose(int verbose)1101 public Builder verbose(int verbose) { 1102 jCommander.setVerbose(verbose); 1103 return this; 1104 } 1105 allowAbbreviatedOptions(boolean b)1106 public Builder allowAbbreviatedOptions(boolean b) { 1107 jCommander.setAllowAbbreviatedOptions(b); 1108 return this; 1109 } 1110 acceptUnknownOptions(boolean b)1111 public Builder acceptUnknownOptions(boolean b) { 1112 jCommander.setAcceptUnknownOptions(b); 1113 return this; 1114 } 1115 allowParameterOverwriting(boolean b)1116 public Builder allowParameterOverwriting(boolean b) { 1117 jCommander.setAllowParameterOverwriting(b); 1118 return this; 1119 } 1120 atFileCharset(Charset charset)1121 public Builder atFileCharset(Charset charset) { 1122 jCommander.setAtFileCharset(charset); 1123 return this; 1124 } 1125 addConverterInstanceFactory(IStringConverterInstanceFactory factory)1126 public Builder addConverterInstanceFactory(IStringConverterInstanceFactory factory) { 1127 jCommander.addConverterInstanceFactory(factory); 1128 return this; 1129 } 1130 addCommand(Object command)1131 public Builder addCommand(Object command) { 1132 jCommander.addCommand(command); 1133 return this; 1134 } 1135 addCommand(String name, Object command, String... aliases)1136 public Builder addCommand(String name, Object command, String... aliases) { 1137 jCommander.addCommand(name, command, aliases); 1138 return this; 1139 } 1140 build()1141 public JCommander build() { 1142 if (args != null) { 1143 jCommander.parse(args); 1144 } 1145 return jCommander; 1146 } 1147 } 1148 1149 1150 /** 1151 * Store the help in the passed string builder. 1152 */ usage(StringBuilder out)1153 public void usage(StringBuilder out) { 1154 usage(out, ""); 1155 } 1156 usage(StringBuilder out, String indent)1157 public void usage(StringBuilder out, String indent) { 1158 if (descriptions == null) createDescriptions(); 1159 boolean hasCommands = !commands.isEmpty(); 1160 boolean hasOptions = !descriptions.isEmpty(); 1161 1162 //indenting 1163 int descriptionIndent = 6; 1164 int indentCount = indent.length() + descriptionIndent; 1165 1166 // 1167 // First line of the usage 1168 // 1169 String programName = this.programName != null ? this.programName.getDisplayName() : "<main class>"; 1170 StringBuilder mainLine = new StringBuilder(); 1171 mainLine.append(indent).append("Usage: ").append(programName); 1172 if (hasOptions) mainLine.append(" [options]"); 1173 if (hasCommands) mainLine.append(indent).append(" [command] [command options]"); 1174 if (mainParameterDescription != null) { 1175 mainLine.append(" ").append(mainParameterDescription.getDescription()); 1176 } 1177 wrapDescription(out, indentCount, mainLine.toString()); 1178 out.append("\n"); 1179 1180 // 1181 // Align the descriptions at the "longestName" column 1182 // 1183 int longestName = 0; 1184 List<ParameterDescription> sorted = Lists.newArrayList(); 1185 for (ParameterDescription pd : fields.values()) { 1186 if (!pd.getParameter().hidden()) { 1187 sorted.add(pd); 1188 // + to have an extra space between the name and the description 1189 int length = pd.getNames().length() + 2; 1190 if (length > longestName) { 1191 longestName = length; 1192 } 1193 } 1194 } 1195 1196 // 1197 // Sort the options 1198 // 1199 Collections.sort(sorted, getParameterDescriptionComparator()); 1200 1201 // 1202 // Display all the names and descriptions 1203 // 1204 if (sorted.size() > 0) out.append(indent).append(" Options:\n"); 1205 for (ParameterDescription pd : sorted) { 1206 WrappedParameter parameter = pd.getParameter(); 1207 out.append(indent).append(" " 1208 + (parameter.required() ? "* " : " ") 1209 + pd.getNames() 1210 + "\n"); 1211 wrapDescription(out, indentCount, s(indentCount) + pd.getDescription()); 1212 Object def = pd.getDefault(); 1213 if (pd.isDynamicParameter()) { 1214 out.append("\n" + s(indentCount)) 1215 .append("Syntax: " + parameter.names()[0] 1216 + "key" + parameter.getAssignment() 1217 + "value"); 1218 } 1219 if (def != null && !pd.isHelp()) { 1220 String displayedDef = Strings.isStringEmpty(def.toString()) 1221 ? "<empty string>" 1222 : def.toString(); 1223 out.append("\n" + s(indentCount)) 1224 .append("Default: " + (parameter.password() ? "********" : displayedDef)); 1225 } 1226 Class<?> type = pd.getParameterized().getType(); 1227 if (type.isEnum()) { 1228 out.append("\n" + s(indentCount)) 1229 .append("Possible Values: " + EnumSet.allOf((Class<? extends Enum>) type)); 1230 } 1231 out.append("\n"); 1232 } 1233 1234 // 1235 // If commands were specified, show them as well 1236 // 1237 if (hasCommands) { 1238 out.append(indent + " Commands:\n"); 1239 // The magic value 3 is the number of spaces between the name of the option 1240 // and its description 1241 for (Map.Entry<ProgramName, JCommander> commands : this.commands.entrySet()) { 1242 Object arg = commands.getValue().getObjects().get(0); 1243 Parameters p = arg.getClass().getAnnotation(Parameters.class); 1244 if (p == null || !p.hidden()) { 1245 ProgramName progName = commands.getKey(); 1246 String dispName = progName.getDisplayName(); 1247 String description = getCommandDescription(progName.getName()); 1248 wrapDescription(out, indentCount + descriptionIndent, 1249 indent + " " + dispName + " " + description); 1250 out.append("\n"); 1251 1252 // Options for this command 1253 JCommander jc = findCommandByAlias(progName.getName()); 1254 jc.usage(out, indent + " "); 1255 out.append("\n"); 1256 } 1257 } 1258 } 1259 } 1260 getParameterDescriptionComparator()1261 private Comparator<? super ParameterDescription> getParameterDescriptionComparator() { 1262 return options.parameterDescriptionComparator; 1263 } 1264 setParameterDescriptionComparator(Comparator<? super ParameterDescription> c)1265 public void setParameterDescriptionComparator(Comparator<? super ParameterDescription> c) { 1266 options.parameterDescriptionComparator = c; 1267 } 1268 setColumnSize(int columnSize)1269 public void setColumnSize(int columnSize) { 1270 options.columnSize = columnSize; 1271 } 1272 getColumnSize()1273 public int getColumnSize() { 1274 return options.columnSize; 1275 } 1276 1277 /** 1278 * Wrap a potentially long line to {@link #getColumnSize()}. 1279 * 1280 * @param out the output 1281 * @param indent the indentation in spaces for lines after the first line. 1282 * @param description the text to wrap. No extra spaces are inserted before {@code 1283 * description}. If the first line needs to be indented prepend the 1284 * correct number of spaces to {@code description}. 1285 */ wrapDescription(StringBuilder out, int indent, String description)1286 private void wrapDescription(StringBuilder out, int indent, String description) { 1287 int max = getColumnSize(); 1288 String[] words = description.split(" "); 1289 int current = 0; 1290 int i = 0; 1291 while (i < words.length) { 1292 String word = words[i]; 1293 if (word.length() > max || current + 1 + word.length() <= max) { 1294 out.append(word); 1295 current += word.length(); 1296 if (i != words.length - 1) { 1297 out.append(" "); 1298 current++; 1299 } 1300 } else { 1301 out.append("\n").append(s(indent)).append(word).append(" "); 1302 current = indent + 1 + word.length(); 1303 } 1304 i++; 1305 } 1306 } 1307 1308 /** 1309 * @return a Collection of all the \@Parameter annotations found on the 1310 * target class. This can be used to display the usage() in a different 1311 * format (e.g. HTML). 1312 */ getParameters()1313 public List<ParameterDescription> getParameters() { 1314 return new ArrayList<>(fields.values()); 1315 } 1316 1317 /** 1318 * @return the main parameter description or null if none is defined. 1319 */ getMainParameter()1320 public ParameterDescription getMainParameter() { 1321 return mainParameterDescription; 1322 } 1323 p(String string)1324 private void p(String string) { 1325 if (options.verbose > 0 || System.getProperty(JCommander.DEBUG_PROPERTY) != null) { 1326 getConsole().println("[JCommander] " + string); 1327 } 1328 } 1329 1330 /** 1331 * Define the default provider for this instance. 1332 */ setDefaultProvider(IDefaultProvider defaultProvider)1333 public void setDefaultProvider(IDefaultProvider defaultProvider) { 1334 options.defaultProvider = defaultProvider; 1335 } 1336 1337 /** 1338 * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. 1339 * @param converterFactory the factory determining string converters 1340 */ addConverterFactory(final IStringConverterFactory converterFactory)1341 public void addConverterFactory(final IStringConverterFactory converterFactory) { 1342 addConverterInstanceFactory(new IStringConverterInstanceFactory() { 1343 @SuppressWarnings("unchecked") 1344 @Override 1345 public IStringConverter<?> getConverterInstance(Parameter parameter, Class<?> forType, String optionName) { 1346 final Class<? extends IStringConverter<?>> converterClass = converterFactory.getConverter(forType); 1347 try { 1348 if(optionName == null) { 1349 optionName = parameter.names().length > 0 ? parameter.names()[0] : "[Main class]"; 1350 } 1351 return converterClass != null ? instantiateConverter(optionName, converterClass) : null; 1352 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 1353 throw new ParameterException(e); 1354 } 1355 } 1356 }); 1357 } 1358 1359 /** 1360 * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. 1361 * @param converterInstanceFactory the factory generating string converter instances 1362 */ addConverterInstanceFactory(IStringConverterInstanceFactory converterInstanceFactory)1363 public void addConverterInstanceFactory(IStringConverterInstanceFactory converterInstanceFactory) { 1364 options.converterInstanceFactories.add(0, converterInstanceFactory); 1365 } 1366 findConverterInstance(Parameter parameter, Class<?> forType, String optionName)1367 private IStringConverter<?> findConverterInstance(Parameter parameter, Class<?> forType, String optionName) { 1368 for (IStringConverterInstanceFactory f : options.converterInstanceFactories) { 1369 IStringConverter<?> result = f.getConverterInstance(parameter, forType, optionName); 1370 if (result != null) return result; 1371 } 1372 1373 return null; 1374 } 1375 1376 /** 1377 * @param type The type of the actual parameter 1378 * @param optionName 1379 * @param value The value to convert 1380 */ convertValue(final Parameterized parameterized, Class type, String optionName, String value)1381 public Object convertValue(final Parameterized parameterized, Class type, String optionName, String value) { 1382 final Parameter annotation = parameterized.getParameter(); 1383 1384 // Do nothing if it's a @DynamicParameter 1385 if (annotation == null) return value; 1386 1387 if(optionName == null) { 1388 optionName = annotation.names().length > 0 ? annotation.names()[0] : "[Main class]"; 1389 } 1390 1391 IStringConverter<?> converter = null; 1392 if (type.isAssignableFrom(List.class)) { 1393 // If a list converter was specified, pass the value to it for direct conversion 1394 converter = tryInstantiateConverter(optionName, annotation.listConverter()); 1395 } 1396 if (type.isAssignableFrom(List.class) && converter == null) { 1397 // No list converter: use the single value converter and pass each parsed value to it individually 1398 final IParameterSplitter splitter = tryInstantiateConverter(null, annotation.splitter()); 1399 converter = new DefaultListConverter(splitter, new IStringConverter() { 1400 @Override 1401 public Object convert(String value) { 1402 final Type genericType = parameterized.findFieldGenericType(); 1403 return convertValue(parameterized, genericType instanceof Class ? (Class) genericType : String.class, null, value); 1404 } 1405 }); 1406 } 1407 1408 if (converter == null) { 1409 converter = tryInstantiateConverter(optionName, annotation.converter()); 1410 } 1411 if (converter == null) { 1412 converter = findConverterInstance(annotation, type, optionName); 1413 } 1414 if (converter == null && type.isEnum()) { 1415 converter = new EnumConverter(optionName, type); 1416 } 1417 if (converter == null) { 1418 converter = new StringConverter(); 1419 } 1420 return converter.convert(value); 1421 } 1422 tryInstantiateConverter(String optionName, Class<T> converterClass)1423 private static <T> T tryInstantiateConverter(String optionName, Class<T> converterClass) { 1424 if (converterClass == NoConverter.class || converterClass == null) { 1425 return null; 1426 } 1427 try { 1428 return instantiateConverter(optionName, converterClass); 1429 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ignore) { 1430 return null; 1431 } 1432 } 1433 instantiateConverter(String optionName, Class<? extends T> converterClass)1434 private static <T> T instantiateConverter(String optionName, Class<? extends T> converterClass) 1435 throws InstantiationException, IllegalAccessException, 1436 InvocationTargetException { 1437 Constructor<T> ctor = null; 1438 Constructor<T> stringCtor = null; 1439 for (Constructor<T> c : (Constructor<T>[]) converterClass.getDeclaredConstructors()) { 1440 c.setAccessible(true); 1441 Class<?>[] types = c.getParameterTypes(); 1442 if (types.length == 1 && types[0].equals(String.class)) { 1443 stringCtor = c; 1444 } else if (types.length == 0) { 1445 ctor = c; 1446 } 1447 } 1448 1449 return stringCtor != null 1450 ? stringCtor.newInstance(optionName) 1451 : ctor != null 1452 ? ctor.newInstance() 1453 : null; 1454 } 1455 1456 /** 1457 * Add a command object. 1458 */ addCommand(String name, Object object)1459 public void addCommand(String name, Object object) { 1460 addCommand(name, object, new String[0]); 1461 } 1462 addCommand(Object object)1463 public void addCommand(Object object) { 1464 Parameters p = object.getClass().getAnnotation(Parameters.class); 1465 if (p != null && p.commandNames().length > 0) { 1466 for (String commandName : p.commandNames()) { 1467 addCommand(commandName, object); 1468 } 1469 } else { 1470 throw new ParameterException("Trying to add command " + object.getClass().getName() 1471 + " without specifying its names in @Parameters"); 1472 } 1473 } 1474 1475 /** 1476 * Add a command object and its aliases. 1477 */ addCommand(String name, Object object, String... aliases)1478 public void addCommand(String name, Object object, String... aliases) { 1479 JCommander jc = new JCommander(options); 1480 jc.addObject(object); 1481 jc.createDescriptions(); 1482 jc.setProgramName(name, aliases); 1483 ProgramName progName = jc.programName; 1484 commands.put(progName, jc); 1485 1486 /* 1487 * Register aliases 1488 */ 1489 //register command name as an alias of itself for reverse lookup 1490 //Note: Name clash check is intentionally omitted to resemble the 1491 // original behaviour of clashing commands. 1492 // Aliases are, however, are strictly checked for name clashes. 1493 aliasMap.put(new StringKey(name), progName); 1494 for (String a : aliases) { 1495 IKey alias = new StringKey(a); 1496 //omit pointless aliases to avoid name clash exception 1497 if (!alias.equals(name)) { 1498 ProgramName mappedName = aliasMap.get(alias); 1499 if (mappedName != null && !mappedName.equals(progName)) { 1500 throw new ParameterException("Cannot set alias " + alias 1501 + " for " + name 1502 + " command because it has already been defined for " 1503 + mappedName.name + " command"); 1504 } 1505 aliasMap.put(alias, progName); 1506 } 1507 } 1508 } 1509 getCommands()1510 public Map<String, JCommander> getCommands() { 1511 Map<String, JCommander> res = Maps.newLinkedHashMap(); 1512 for (Map.Entry<ProgramName, JCommander> entry : commands.entrySet()) { 1513 res.put(entry.getKey().name, entry.getValue()); 1514 } 1515 return res; 1516 } 1517 getParsedCommand()1518 public String getParsedCommand() { 1519 return parsedCommand; 1520 } 1521 1522 /** 1523 * The name of the command or the alias in the form it was 1524 * passed to the command line. <code>null</code> if no 1525 * command or alias was specified. 1526 * 1527 * @return Name of command or alias passed to command line. If none passed: <code>null</code>. 1528 */ getParsedAlias()1529 public String getParsedAlias() { 1530 return parsedAlias; 1531 } 1532 1533 /** 1534 * @return n spaces 1535 */ s(int count)1536 private String s(int count) { 1537 StringBuilder result = new StringBuilder(); 1538 for (int i = 0; i < count; i++) { 1539 result.append(" "); 1540 } 1541 1542 return result.toString(); 1543 } 1544 1545 /** 1546 * @return the objects that JCommander will fill with the result of 1547 * parsing the command line. 1548 */ getObjects()1549 public List<Object> getObjects() { 1550 return objects; 1551 } 1552 findParameterDescription(String arg)1553 private ParameterDescription findParameterDescription(String arg) { 1554 return FuzzyMap.findInMap(descriptions, new StringKey(arg), 1555 options.caseSensitiveOptions, options.allowAbbreviatedOptions); 1556 } 1557 findCommand(ProgramName name)1558 private JCommander findCommand(ProgramName name) { 1559 return FuzzyMap.findInMap(commands, name, 1560 options.caseSensitiveOptions, options.allowAbbreviatedOptions); 1561 } 1562 findProgramName(String name)1563 private ProgramName findProgramName(String name) { 1564 return FuzzyMap.findInMap(aliasMap, new StringKey(name), 1565 options.caseSensitiveOptions, options.allowAbbreviatedOptions); 1566 } 1567 1568 /* 1569 * Reverse lookup JCommand object by command's name or its alias 1570 */ findCommandByAlias(String commandOrAlias)1571 private JCommander findCommandByAlias(String commandOrAlias) { 1572 ProgramName progName = findProgramName(commandOrAlias); 1573 if (progName == null) { 1574 return null; 1575 } 1576 JCommander jc = findCommand(progName); 1577 if (jc == null) { 1578 throw new IllegalStateException( 1579 "There appears to be inconsistency in the internal command database. " + 1580 " This is likely a bug. Please report."); 1581 } 1582 return jc; 1583 } 1584 1585 /** 1586 * Encapsulation of either a main application or an individual command. 1587 */ 1588 private static final class ProgramName implements IKey { 1589 private final String name; 1590 private final List<String> aliases; 1591 ProgramName(String name, List<String> aliases)1592 ProgramName(String name, List<String> aliases) { 1593 this.name = name; 1594 this.aliases = aliases; 1595 } 1596 1597 @Override getName()1598 public String getName() { 1599 return name; 1600 } 1601 getDisplayName()1602 private String getDisplayName() { 1603 StringBuilder sb = new StringBuilder(); 1604 sb.append(name); 1605 if (!aliases.isEmpty()) { 1606 sb.append("("); 1607 Iterator<String> aliasesIt = aliases.iterator(); 1608 while (aliasesIt.hasNext()) { 1609 sb.append(aliasesIt.next()); 1610 if (aliasesIt.hasNext()) { 1611 sb.append(","); 1612 } 1613 } 1614 sb.append(")"); 1615 } 1616 return sb.toString(); 1617 } 1618 1619 @Override hashCode()1620 public int hashCode() { 1621 final int prime = 31; 1622 int result = 1; 1623 result = prime * result + ((name == null) ? 0 : name.hashCode()); 1624 return result; 1625 } 1626 1627 @Override equals(Object obj)1628 public boolean equals(Object obj) { 1629 if (this == obj) 1630 return true; 1631 if (obj == null) 1632 return false; 1633 if (getClass() != obj.getClass()) 1634 return false; 1635 ProgramName other = (ProgramName) obj; 1636 if (name == null) { 1637 if (other.name != null) 1638 return false; 1639 } else if (!name.equals(other.name)) 1640 return false; 1641 return true; 1642 } 1643 1644 /* 1645 * Important: ProgramName#toString() is used by longestName(Collection) function 1646 * to format usage output. 1647 */ 1648 @Override toString()1649 public String toString() { 1650 return getDisplayName(); 1651 1652 } 1653 } 1654 setVerbose(int verbose)1655 public void setVerbose(int verbose) { 1656 options.verbose = verbose; 1657 } 1658 setCaseSensitiveOptions(boolean b)1659 public void setCaseSensitiveOptions(boolean b) { 1660 options.caseSensitiveOptions = b; 1661 } 1662 setAllowAbbreviatedOptions(boolean b)1663 public void setAllowAbbreviatedOptions(boolean b) { 1664 options.allowAbbreviatedOptions = b; 1665 } 1666 setAcceptUnknownOptions(boolean b)1667 public void setAcceptUnknownOptions(boolean b) { 1668 options.acceptUnknownOptions = b; 1669 } 1670 getUnknownOptions()1671 public List<String> getUnknownOptions() { 1672 return unknownArgs; 1673 } 1674 setAllowParameterOverwriting(boolean b)1675 public void setAllowParameterOverwriting(boolean b) { 1676 options.allowParameterOverwriting = b; 1677 } 1678 isParameterOverwritingAllowed()1679 public boolean isParameterOverwritingAllowed() { 1680 return options.allowParameterOverwriting; 1681 } 1682 1683 /** 1684 * Sets the charset used to expand {@code @files}. 1685 * @param charset the charset 1686 */ setAtFileCharset(Charset charset)1687 public void setAtFileCharset(Charset charset) { 1688 options.atFileCharset = charset; 1689 } 1690 1691 } 1692