1 // Copyright 2014 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.devtools.common.options; 16 17 import com.google.common.base.Function; 18 import com.google.common.base.Functions; 19 import com.google.common.base.Joiner; 20 import com.google.common.base.Preconditions; 21 import com.google.common.base.Predicate; 22 import com.google.common.collect.ArrayListMultimap; 23 import com.google.common.collect.ImmutableList; 24 import com.google.common.collect.Iterables; 25 import com.google.common.collect.Iterators; 26 import com.google.common.collect.LinkedHashMultimap; 27 import com.google.common.collect.Lists; 28 import com.google.common.collect.Maps; 29 import com.google.common.collect.Multimap; 30 import com.google.devtools.common.options.OptionsParser.OptionDescription; 31 import com.google.devtools.common.options.OptionsParser.OptionValueDescription; 32 import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription; 33 import java.lang.reflect.Constructor; 34 import java.lang.reflect.Field; 35 import java.util.Arrays; 36 import java.util.Collection; 37 import java.util.Collections; 38 import java.util.Comparator; 39 import java.util.Iterator; 40 import java.util.LinkedHashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * The implementation of the options parser. This is intentionally package 46 * private for full flexibility. Use {@link OptionsParser} or {@link Options} 47 * if you're a consumer. 48 */ 49 class OptionsParserImpl { 50 51 private final OptionsData optionsData; 52 53 /** 54 * We store the results of parsing the arguments in here. It'll look like 55 * 56 * <pre> 57 * Field("--host") -> "www.google.com" 58 * Field("--port") -> 80 59 * </pre> 60 * 61 * This map is modified by repeated calls to {@link #parse(OptionPriority,Function,List)}. 62 */ 63 private final Map<Field, OptionValueDescription> parsedValues = Maps.newHashMap(); 64 65 /** 66 * We store the pre-parsed, explicit options for each priority in here. 67 * We use partially preparsed options, which can be different from the original 68 * representation, e.g. "--nofoo" becomes "--foo=0". 69 */ 70 private final List<UnparsedOptionValueDescription> unparsedValues = Lists.newArrayList(); 71 72 /** 73 * Unparsed values for use with the canonicalize command are stored separately from 74 * unparsedValues so that invocation policy can modify the values for canonicalization (e.g. 75 * override user-specified values with default values) without corrupting the data used to 76 * represent the user's original invocation for {@link #asListOfExplicitOptions()} and 77 * {@link #asListOfUnparsedOptions()}. A LinkedHashMultimap is used so that canonicalization 78 * happens in the correct order and multiple values can be stored for flags that allow multiple 79 * values. 80 */ 81 private final Multimap<Field, UnparsedOptionValueDescription> canonicalizeValues 82 = LinkedHashMultimap.create(); 83 84 private final List<String> warnings = Lists.newArrayList(); 85 86 private boolean allowSingleDashLongOptions = false; 87 88 private ArgsPreProcessor argsPreProcessor = 89 new ArgsPreProcessor() { 90 @Override 91 public List<String> preProcess(List<String> args) throws OptionsParsingException { 92 return args; 93 } 94 }; 95 96 /** 97 * Create a new parser object 98 */ OptionsParserImpl(OptionsData optionsData)99 OptionsParserImpl(OptionsData optionsData) { 100 this.optionsData = optionsData; 101 } 102 getOptionsData()103 OptionsData getOptionsData() { 104 return optionsData; 105 } 106 107 /** 108 * Indicates whether or not the parser will allow long options with a 109 * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example. 110 */ setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions)111 void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) { 112 this.allowSingleDashLongOptions = allowSingleDashLongOptions; 113 } 114 115 /** Sets the ArgsPreProcessor for manipulations of the options before parsing. */ setArgsPreProcessor(ArgsPreProcessor preProcessor)116 void setArgsPreProcessor(ArgsPreProcessor preProcessor) { 117 this.argsPreProcessor = Preconditions.checkNotNull(preProcessor); 118 } 119 120 /** 121 * The implementation of {@link OptionsBase#asMap}. 122 */ optionsAsMap(OptionsBase optionsInstance)123 static Map<String, Object> optionsAsMap(OptionsBase optionsInstance) { 124 Map<String, Object> map = Maps.newHashMap(); 125 for (Field field : OptionsParser.getAllAnnotatedFields(optionsInstance.getClass())) { 126 try { 127 String name = field.getAnnotation(Option.class).name(); 128 Object value = field.get(optionsInstance); 129 map.put(name, value); 130 } catch (IllegalAccessException e) { 131 throw new IllegalStateException(e); // unreachable 132 } 133 } 134 return map; 135 } 136 getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz)137 List<Field> getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz) { 138 return optionsData.getFieldsForClass(clazz); 139 } 140 141 /** 142 * Implements {@link OptionsParser#asListOfUnparsedOptions()}. 143 */ asListOfUnparsedOptions()144 List<UnparsedOptionValueDescription> asListOfUnparsedOptions() { 145 List<UnparsedOptionValueDescription> result = Lists.newArrayList(unparsedValues); 146 // It is vital that this sort is stable so that options on the same priority are not reordered. 147 Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() { 148 @Override 149 public int compare(UnparsedOptionValueDescription o1, 150 UnparsedOptionValueDescription o2) { 151 return o1.getPriority().compareTo(o2.getPriority()); 152 } 153 }); 154 return result; 155 } 156 157 /** 158 * Implements {@link OptionsParser#asListOfExplicitOptions()}. 159 */ asListOfExplicitOptions()160 List<UnparsedOptionValueDescription> asListOfExplicitOptions() { 161 List<UnparsedOptionValueDescription> result = Lists.newArrayList(Iterables.filter( 162 unparsedValues, 163 new Predicate<UnparsedOptionValueDescription>() { 164 @Override 165 public boolean apply(UnparsedOptionValueDescription input) { 166 return input.isExplicit(); 167 } 168 })); 169 // It is vital that this sort is stable so that options on the same priority are not reordered. 170 Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() { 171 @Override 172 public int compare(UnparsedOptionValueDescription o1, 173 UnparsedOptionValueDescription o2) { 174 return o1.getPriority().compareTo(o2.getPriority()); 175 } 176 }); 177 return result; 178 } 179 180 /** 181 * Implements {@link OptionsParser#canonicalize}. 182 */ asCanonicalizedList()183 List<String> asCanonicalizedList() { 184 185 List<UnparsedOptionValueDescription> processed = Lists.newArrayList( 186 canonicalizeValues.values()); 187 // Sort implicit requirement options to the end, keeping their existing order, and sort the 188 // other options alphabetically. 189 Collections.sort(processed, new Comparator<UnparsedOptionValueDescription>() { 190 @Override 191 public int compare(UnparsedOptionValueDescription o1, UnparsedOptionValueDescription o2) { 192 if (o1.isImplicitRequirement()) { 193 return o2.isImplicitRequirement() ? 0 : 1; 194 } 195 if (o2.isImplicitRequirement()) { 196 return -1; 197 } 198 return o1.getName().compareTo(o2.getName()); 199 } 200 }); 201 202 List<String> result = Lists.newArrayList(); 203 for (UnparsedOptionValueDescription value : processed) { 204 205 // Ignore expansion options. 206 if (value.isExpansion()) { 207 continue; 208 } 209 210 result.add("--" + value.getName() + "=" + value.getUnparsedValue()); 211 } 212 return result; 213 } 214 215 /** 216 * Implements {@link OptionsParser#asListOfEffectiveOptions()}. 217 */ asListOfEffectiveOptions()218 List<OptionValueDescription> asListOfEffectiveOptions() { 219 List<OptionValueDescription> result = Lists.newArrayList(); 220 for (Map.Entry<String, Field> mapEntry : optionsData.getAllNamedFields()) { 221 String fieldName = mapEntry.getKey(); 222 Field field = mapEntry.getValue(); 223 OptionValueDescription entry = parsedValues.get(field); 224 if (entry == null) { 225 Object value = optionsData.getDefaultValue(field); 226 result.add( 227 new OptionValueDescription( 228 fieldName, value, OptionPriority.DEFAULT, null, null, null, false)); 229 } else { 230 result.add(entry); 231 } 232 } 233 return result; 234 } 235 getOptionsClasses()236 Collection<Class<? extends OptionsBase>> getOptionsClasses() { 237 return optionsData.getOptionsClasses(); 238 } 239 maybeAddDeprecationWarning(Field field)240 private void maybeAddDeprecationWarning(Field field) { 241 Option option = field.getAnnotation(Option.class); 242 // Continue to support the old behavior for @Deprecated options. 243 String warning = option.deprecationWarning(); 244 if (!warning.isEmpty() || (field.getAnnotation(Deprecated.class) != null)) { 245 addDeprecationWarning(option.name(), warning); 246 } 247 } 248 addDeprecationWarning(String optionName, String warning)249 private void addDeprecationWarning(String optionName, String warning) { 250 warnings.add("Option '" + optionName + "' is deprecated" 251 + (warning.isEmpty() ? "" : ": " + warning)); 252 } 253 254 // Warnings should not end with a '.' because the internal reporter adds one automatically. setValue(Field field, String name, Object value, OptionPriority priority, String source, String implicitDependant, String expandedFrom)255 private void setValue(Field field, String name, Object value, 256 OptionPriority priority, String source, String implicitDependant, String expandedFrom) { 257 OptionValueDescription entry = parsedValues.get(field); 258 if (entry != null) { 259 // Override existing option if the new value has higher or equal priority. 260 if (priority.compareTo(entry.getPriority()) >= 0) { 261 // Output warnings: 262 if ((implicitDependant != null) && (entry.getImplicitDependant() != null)) { 263 if (!implicitDependant.equals(entry.getImplicitDependant())) { 264 warnings.add( 265 "Option '" 266 + name 267 + "' is implicitly defined by both option '" 268 + entry.getImplicitDependant() 269 + "' and option '" 270 + implicitDependant 271 + "'"); 272 } 273 } else if ((implicitDependant != null) && priority.equals(entry.getPriority())) { 274 warnings.add( 275 "Option '" 276 + name 277 + "' is implicitly defined by option '" 278 + implicitDependant 279 + "'; the implicitly set value overrides the previous one"); 280 } else if (entry.getImplicitDependant() != null) { 281 warnings.add( 282 "A new value for option '" 283 + name 284 + "' overrides a previous implicit setting of that option by option '" 285 + entry.getImplicitDependant() 286 + "'"); 287 } else if ((priority == entry.getPriority()) 288 && ((entry.getExpansionParent() == null) && (expandedFrom != null))) { 289 // Create a warning if an expansion option overrides an explicit option: 290 warnings.add("The option '" + expandedFrom + "' was expanded and now overrides a " 291 + "previous explicitly specified option '" + name + "'"); 292 } 293 294 // Record the new value: 295 parsedValues.put( 296 field, 297 new OptionValueDescription( 298 name, value, priority, source, implicitDependant, expandedFrom, false)); 299 } 300 } else { 301 parsedValues.put( 302 field, 303 new OptionValueDescription( 304 name, value, priority, source, implicitDependant, expandedFrom, false)); 305 maybeAddDeprecationWarning(field); 306 } 307 } 308 addListValue(Field field, Object value, OptionPriority priority, String source, String implicitDependant, String expandedFrom)309 private void addListValue(Field field, Object value, OptionPriority priority, String source, 310 String implicitDependant, String expandedFrom) { 311 OptionValueDescription entry = parsedValues.get(field); 312 if (entry == null) { 313 entry = 314 new OptionValueDescription( 315 field.getName(), 316 ArrayListMultimap.create(), 317 priority, 318 source, 319 implicitDependant, 320 expandedFrom, 321 true); 322 parsedValues.put(field, entry); 323 maybeAddDeprecationWarning(field); 324 } 325 entry.addValue(priority, value); 326 } 327 clearValue(String optionName, Map<String, OptionValueDescription> clearedValues)328 void clearValue(String optionName, Map<String, OptionValueDescription> clearedValues) 329 throws OptionsParsingException { 330 Field field = optionsData.getFieldFromName(optionName); 331 if (field == null) { 332 throw new IllegalArgumentException("No such option '" + optionName + "'"); 333 } 334 Option option = field.getAnnotation(Option.class); 335 clearValue(field, option, clearedValues); 336 } 337 clearValue( Field field, Option option, Map<String, OptionValueDescription> clearedValues)338 private void clearValue( 339 Field field, Option option, Map<String, OptionValueDescription> clearedValues) 340 throws OptionsParsingException { 341 342 OptionValueDescription removed = parsedValues.remove(field); 343 if (removed != null) { 344 clearedValues.put(option.name(), removed); 345 } 346 347 canonicalizeValues.removeAll(field); 348 349 // Recurse to remove any implicit or expansion flags that this flag may have added when 350 // originally parsed. 351 String[] expansion = optionsData.getEvaluatedExpansion(field); 352 for (String[] args : new String[][] {option.implicitRequirements(), expansion}) { 353 Iterator<String> argsIterator = Iterators.forArray(args); 354 while (argsIterator.hasNext()) { 355 String arg = argsIterator.next(); 356 ParseOptionResult parseOptionResult = parseOption(arg, argsIterator); 357 clearValue(parseOptionResult.field, parseOptionResult.option, clearedValues); 358 } 359 } 360 } 361 getOptionValueDescription(String name)362 OptionValueDescription getOptionValueDescription(String name) { 363 Field field = optionsData.getFieldFromName(name); 364 if (field == null) { 365 throw new IllegalArgumentException("No such option '" + name + "'"); 366 } 367 return parsedValues.get(field); 368 } 369 getOptionDescription(String name)370 OptionDescription getOptionDescription(String name) { 371 Field field = optionsData.getFieldFromName(name); 372 if (field == null) { 373 return null; 374 } 375 376 Option optionAnnotation = field.getAnnotation(Option.class); 377 return new OptionDescription( 378 name, 379 optionsData.getDefaultValue(field), 380 optionsData.getConverter(field), 381 optionAnnotation.allowMultiple()); 382 } 383 containsExplicitOption(String name)384 boolean containsExplicitOption(String name) { 385 Field field = optionsData.getFieldFromName(name); 386 if (field == null) { 387 throw new IllegalArgumentException("No such option '" + name + "'"); 388 } 389 return parsedValues.get(field) != null; 390 } 391 392 /** 393 * Parses the args, and returns what it doesn't parse. May be called multiple 394 * times, and may be called recursively. In each call, there may be no 395 * duplicates, but separate calls may contain intersecting sets of options; in 396 * that case, the arg seen last takes precedence. 397 */ parse(OptionPriority priority, Function<? super String, String> sourceFunction, List<String> args)398 List<String> parse(OptionPriority priority, Function<? super String, String> sourceFunction, 399 List<String> args) throws OptionsParsingException { 400 return parse(priority, sourceFunction, null, null, args); 401 } 402 403 /** 404 * Parses the args, and returns what it doesn't parse. May be called multiple 405 * times, and may be called recursively. Calls may contain intersecting sets 406 * of options; in that case, the arg seen last takes precedence. 407 * 408 * <p>The method uses the invariant that if an option has neither an implicit 409 * dependent nor an expanded from value, then it must have been explicitly 410 * set. 411 */ parse( OptionPriority priority, Function<? super String, String> sourceFunction, String implicitDependent, String expandedFrom, List<String> args)412 private List<String> parse( 413 OptionPriority priority, 414 Function<? super String, String> sourceFunction, 415 String implicitDependent, 416 String expandedFrom, 417 List<String> args) throws OptionsParsingException { 418 419 List<String> unparsedArgs = Lists.newArrayList(); 420 LinkedHashMap<String,List<String>> implicitRequirements = Maps.newLinkedHashMap(); 421 422 Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator(); 423 while (argsIterator.hasNext()) { 424 String arg = argsIterator.next(); 425 426 if (!arg.startsWith("-")) { 427 unparsedArgs.add(arg); 428 continue; // not an option arg 429 } 430 431 if (arg.equals("--")) { // "--" means all remaining args aren't options 432 Iterators.addAll(unparsedArgs, argsIterator); 433 break; 434 } 435 436 ParseOptionResult optionParseResult = parseOption(arg, argsIterator); 437 Field field = optionParseResult.field; 438 Option option = optionParseResult.option; 439 String value = optionParseResult.value; 440 441 final String originalName = option.name(); 442 443 if (option.wrapperOption()) { 444 if (value.startsWith("-")) { 445 446 List<String> unparsed = parse( 447 priority, 448 Functions.constant("Unwrapped from wrapper option --" + originalName), 449 null, // implicitDependent 450 null, // expandedFrom 451 ImmutableList.of(value)); 452 453 if (!unparsed.isEmpty()) { 454 throw new OptionsParsingException("Unparsed options remain after unwrapping " + 455 arg + ": " + Joiner.on(' ').join(unparsed)); 456 } 457 458 // Don't process implicitRequirements or expansions for wrapper options. In particular, 459 // don't record this option in unparsedValues, so that only the wrapped option shows 460 // up in canonicalized options. 461 continue; 462 463 } else { 464 throw new OptionsParsingException("Invalid --" + originalName + " value format. " 465 + "You may have meant --" + originalName + "=--" + value); 466 } 467 } 468 469 if (implicitDependent == null) { 470 // Log explicit options and expanded options in the order they are parsed (can be sorted 471 // later). Also remember whether they were expanded or not. This information is needed to 472 // correctly canonicalize flags. 473 UnparsedOptionValueDescription unparsedOptionValueDescription = 474 new UnparsedOptionValueDescription( 475 originalName, 476 field, 477 value, 478 priority, 479 sourceFunction.apply(originalName), 480 expandedFrom == null); 481 unparsedValues.add(unparsedOptionValueDescription); 482 if (option.allowMultiple()) { 483 canonicalizeValues.put(field, unparsedOptionValueDescription); 484 } else { 485 canonicalizeValues.replaceValues(field, ImmutableList.of(unparsedOptionValueDescription)); 486 } 487 } 488 489 // Handle expansion options. 490 String[] expansion = optionsData.getEvaluatedExpansion(field); 491 if (expansion.length > 0) { 492 Function<Object, String> expansionSourceFunction = 493 Functions.constant( 494 "expanded from option --" 495 + originalName 496 + " from " 497 + sourceFunction.apply(originalName)); 498 maybeAddDeprecationWarning(field); 499 List<String> unparsed = parse(priority, expansionSourceFunction, null, originalName, 500 ImmutableList.copyOf(expansion)); 501 if (!unparsed.isEmpty()) { 502 // Throw an assertion, because this indicates an error in the code that specified the 503 // expansion for the current option. 504 throw new AssertionError("Unparsed options remain after parsing expansion of " + 505 arg + ": " + Joiner.on(' ').join(unparsed)); 506 } 507 } else { 508 Converter<?> converter = optionsData.getConverter(field); 509 Object convertedValue; 510 try { 511 convertedValue = converter.convert(value); 512 } catch (OptionsParsingException e) { 513 // The converter doesn't know the option name, so we supply it here by 514 // re-throwing: 515 throw new OptionsParsingException("While parsing option " + arg 516 + ": " + e.getMessage(), e); 517 } 518 519 // ...but allow duplicates of single-use options across separate calls to 520 // parse(); latest wins: 521 if (!option.allowMultiple()) { 522 setValue(field, originalName, convertedValue, 523 priority, sourceFunction.apply(originalName), implicitDependent, expandedFrom); 524 } else { 525 // But if it's a multiple-use option, then just accumulate the 526 // values, in the order in which they were seen. 527 // Note: The type of the list member is not known; Java introspection 528 // only makes it available in String form via the signature string 529 // for the field declaration. 530 addListValue(field, convertedValue, priority, sourceFunction.apply(originalName), 531 implicitDependent, expandedFrom); 532 } 533 } 534 535 // Collect any implicit requirements. 536 if (option.implicitRequirements().length > 0) { 537 implicitRequirements.put(option.name(), Arrays.asList(option.implicitRequirements())); 538 } 539 } 540 541 // Now parse any implicit requirements that were collected. 542 // TODO(bazel-team): this should happen when the option is encountered. 543 if (!implicitRequirements.isEmpty()) { 544 for (Map.Entry<String, List<String>> entry : implicitRequirements.entrySet()) { 545 Function<Object, String> requirementSourceFunction = 546 Functions.constant( 547 "implicit requirement of option --" 548 + entry.getKey() 549 + " from " 550 + sourceFunction.apply(entry.getKey())); 551 552 List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null, 553 entry.getValue()); 554 if (!unparsed.isEmpty()) { 555 // Throw an assertion, because this indicates an error in the code that specified in the 556 // implicit requirements for the option(s). 557 throw new AssertionError("Unparsed options remain after parsing implicit options: " 558 + Joiner.on(' ').join(unparsed)); 559 } 560 } 561 } 562 563 return unparsedArgs; 564 } 565 566 private static final class ParseOptionResult { 567 final Field field; 568 final Option option; 569 final String value; 570 ParseOptionResult(Field field, Option option, String value)571 ParseOptionResult(Field field, Option option, String value) { 572 this.field = field; 573 this.option = option; 574 this.value = value; 575 } 576 } 577 parseOption(String arg, Iterator<String> nextArgs)578 private ParseOptionResult parseOption(String arg, Iterator<String> nextArgs) 579 throws OptionsParsingException { 580 581 String value = null; 582 Field field; 583 boolean booleanValue = true; 584 585 if (arg.length() == 2) { // -l (may be nullary or unary) 586 field = optionsData.getFieldForAbbrev(arg.charAt(1)); 587 booleanValue = true; 588 589 } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l- (boolean) 590 field = optionsData.getFieldForAbbrev(arg.charAt(1)); 591 booleanValue = false; 592 593 } else if (allowSingleDashLongOptions // -long_option 594 || arg.startsWith("--")) { // or --long_option 595 596 int equalsAt = arg.indexOf('='); 597 int nameStartsAt = arg.startsWith("--") ? 2 : 1; 598 String name = 599 equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt); 600 if (name.trim().isEmpty()) { 601 throw new OptionsParsingException("Invalid options syntax: " + arg, arg); 602 } 603 value = equalsAt == -1 ? null : arg.substring(equalsAt + 1); 604 field = optionsData.getFieldFromName(name); 605 606 // look for a "no"-prefixed option name: "no<optionname>"; 607 // (Undocumented: we also allow --no_foo. We're generous like that.) 608 if (field == null && name.startsWith("no")) { 609 name = name.substring(name.startsWith("no_") ? 3 : 2); 610 field = optionsData.getFieldFromName(name); 611 booleanValue = false; 612 if (field != null) { 613 // TODO(bazel-team): Add tests for these cases. 614 if (!OptionsData.isBooleanField(field)) { 615 throw new OptionsParsingException( 616 "Illegal use of 'no' prefix on non-boolean option: " + arg, arg); 617 } 618 if (value != null) { 619 throw new OptionsParsingException( 620 "Unexpected value after boolean option: " + arg, arg); 621 } 622 // "no<optionname>" signifies a boolean option w/ false value 623 value = "0"; 624 } 625 } 626 } else { 627 throw new OptionsParsingException("Invalid options syntax: " + arg, arg); 628 } 629 630 Option option = field == null ? null : field.getAnnotation(Option.class); 631 632 if (option == null 633 || OptionsParser.documentationLevel(option.category()) 634 == OptionsParser.DocumentationLevel.INTERNAL) { 635 // This also covers internal options, which are treated as if they did not exist. 636 throw new OptionsParsingException("Unrecognized option: " + arg, arg); 637 } 638 639 if (value == null) { 640 // Special-case boolean to supply value based on presence of "no" prefix. 641 if (OptionsData.isBooleanField(field)) { 642 value = booleanValue ? "1" : "0"; 643 } else if (field.getType().equals(Void.class) && !option.wrapperOption()) { 644 // This is expected, Void type options have no args (unless they're wrapper options). 645 } else if (nextArgs.hasNext()) { 646 value = nextArgs.next(); // "--flag value" form 647 } else { 648 throw new OptionsParsingException("Expected value after " + arg); 649 } 650 } 651 652 return new ParseOptionResult(field, option, value); 653 } 654 655 /** 656 * Gets the result of parsing the options. 657 */ getParsedOptions(Class<O> optionsClass)658 <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) { 659 // Create the instance: 660 O optionsInstance; 661 try { 662 Constructor<O> constructor = optionsData.getConstructor(optionsClass); 663 if (constructor == null) { 664 return null; 665 } 666 optionsInstance = constructor.newInstance(new Object[0]); 667 } catch (Exception e) { 668 throw new IllegalStateException(e); 669 } 670 671 // Set the fields 672 for (Field field : optionsData.getFieldsForClass(optionsClass)) { 673 Object value; 674 OptionValueDescription entry = parsedValues.get(field); 675 if (entry == null) { 676 value = optionsData.getDefaultValue(field); 677 } else { 678 value = entry.getValue(); 679 } 680 try { 681 field.set(optionsInstance, value); 682 } catch (IllegalAccessException e) { 683 throw new IllegalStateException(e); 684 } 685 } 686 return optionsInstance; 687 } 688 getWarnings()689 List<String> getWarnings() { 690 return ImmutableList.copyOf(warnings); 691 } 692 getDefaultOptionString(Field optionField)693 static String getDefaultOptionString(Field optionField) { 694 Option annotation = optionField.getAnnotation(Option.class); 695 return annotation.defaultValue(); 696 } 697 isSpecialNullDefault(String defaultValueString, Field optionField)698 static boolean isSpecialNullDefault(String defaultValueString, Field optionField) { 699 return defaultValueString.equals("null") && !optionField.getType().isPrimitive(); 700 } 701 } 702