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.collect.ImmutableList; 22 import com.google.common.collect.ListMultimap; 23 import com.google.common.escape.Escaper; 24 import java.lang.reflect.Constructor; 25 import java.lang.reflect.Field; 26 import java.nio.file.FileSystem; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collection; 30 import java.util.Collections; 31 import java.util.Comparator; 32 import java.util.HashMap; 33 import java.util.LinkedHashMap; 34 import java.util.LinkedHashSet; 35 import java.util.List; 36 import java.util.Map; 37 import javax.annotation.Nullable; 38 39 /** 40 * A parser for options. Typical use case in a main method: 41 * 42 * <pre> 43 * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class, BarOptions.class); 44 * parser.parseAndExitUponError(args); 45 * FooOptions foo = parser.getOptions(FooOptions.class); 46 * BarOptions bar = parser.getOptions(BarOptions.class); 47 * List<String> otherArguments = parser.getResidue(); 48 * </pre> 49 * 50 * <p>FooOptions and BarOptions would be options specification classes, derived from OptionsBase, 51 * that contain fields annotated with @Option(...). 52 * 53 * <p>Alternatively, rather than calling {@link #parseAndExitUponError(OptionPriority, String, 54 * String[])}, client code may call {@link #parse(OptionPriority,String,List)}, and handle parser 55 * exceptions usage messages themselves. 56 * 57 * <p>This options parsing implementation has (at least) one design flaw. It allows both '--foo=baz' 58 * and '--foo baz' for all options except void, boolean and tristate options. For these, the 'baz' 59 * in '--foo baz' is not treated as a parameter to the option, making it is impossible to switch 60 * options between void/boolean/tristate and everything else without breaking backwards 61 * compatibility. 62 * 63 * @see Options a simpler class which you can use if you only have one options specification class 64 */ 65 public class OptionsParser implements OptionsProvider { 66 67 /** 68 * An unchecked exception thrown when there is a problem constructing a parser, e.g. an error 69 * while validating an {@link Option} field in one of its {@link OptionsBase} subclasses. 70 * 71 * <p>This exception is unchecked because it generally indicates an internal error affecting all 72 * invocations of the program. I.e., any such error should be immediately obvious to the 73 * developer. Although unchecked, we explicitly mark some methods as throwing it as a reminder in 74 * the API. 75 */ 76 public static class ConstructionException extends RuntimeException { 77 public ConstructionException(String message) { 78 super(message); 79 } 80 81 public ConstructionException(Throwable cause) { 82 super(cause); 83 } 84 85 public ConstructionException(String message, Throwable cause) { 86 super(message, cause); 87 } 88 } 89 90 /** 91 * A cache for the parsed options data. Both keys and values are immutable, so 92 * this is always safe. Only access this field through the {@link 93 * #getOptionsData} method for thread-safety! The cache is very unlikely to 94 * grow to a significant amount of memory, because there's only a fixed set of 95 * options classes on the classpath. 96 */ 97 private static final Map<ImmutableList<Class<? extends OptionsBase>>, OptionsData> optionsData = 98 new HashMap<>(); 99 100 /** 101 * Returns {@link OpaqueOptionsData} suitable for passing along to {@link 102 * #newOptionsParser(OpaqueOptionsData optionsData)}. 103 * 104 * <p>This is useful when you want to do the work of analyzing the given {@code optionsClasses} 105 * exactly once, but you want to parse lots of different lists of strings (and thus need to 106 * construct lots of different {@link OptionsParser} instances). 107 */ 108 public static OpaqueOptionsData getOptionsData( 109 List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { 110 return getOptionsDataInternal(optionsClasses); 111 } 112 113 /** 114 * Returns the {@link OptionsData} associated with the given list of options classes. 115 */ 116 static synchronized OptionsData getOptionsDataInternal( 117 List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { 118 ImmutableList<Class<? extends OptionsBase>> immutableOptionsClasses = 119 ImmutableList.copyOf(optionsClasses); 120 OptionsData result = optionsData.get(immutableOptionsClasses); 121 if (result == null) { 122 try { 123 result = OptionsData.from(immutableOptionsClasses); 124 } catch (Exception e) { 125 throw new ConstructionException(e.getMessage(), e); 126 } 127 optionsData.put(immutableOptionsClasses, result); 128 } 129 return result; 130 } 131 132 /** 133 * Returns the {@link OptionsData} associated with the given options class. 134 */ 135 static OptionsData getOptionsDataInternal(Class<? extends OptionsBase> optionsClass) 136 throws ConstructionException { 137 return getOptionsDataInternal(ImmutableList.<Class<? extends OptionsBase>>of(optionsClass)); 138 } 139 140 /** 141 * @see #newOptionsParser(Iterable) 142 */ 143 public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1) 144 throws ConstructionException { 145 return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1)); 146 } 147 148 /** 149 * @see #newOptionsParser(Iterable) 150 */ 151 public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1, 152 Class<? extends OptionsBase> class2) 153 throws ConstructionException { 154 return newOptionsParser(ImmutableList.of(class1, class2)); 155 } 156 157 /** Create a new {@link OptionsParser}. */ 158 public static OptionsParser newOptionsParser( 159 Iterable<? extends Class<? extends OptionsBase>> optionsClasses) 160 throws ConstructionException { 161 return newOptionsParser( 162 getOptionsDataInternal(ImmutableList.<Class<? extends OptionsBase>>copyOf(optionsClasses))); 163 } 164 165 /** 166 * Create a new {@link OptionsParser}, using {@link OpaqueOptionsData} previously returned from 167 * {@link #getOptionsData}. 168 */ 169 public static OptionsParser newOptionsParser(OpaqueOptionsData optionsData) { 170 return new OptionsParser((OptionsData) optionsData); 171 } 172 173 private final OptionsParserImpl impl; 174 private final List<String> residue = new ArrayList<String>(); 175 private boolean allowResidue = true; 176 177 OptionsParser(OptionsData optionsData) { 178 impl = new OptionsParserImpl(optionsData); 179 } 180 181 /** 182 * Indicates whether or not the parser will allow a non-empty residue; that 183 * is, iff this value is true then a call to one of the {@code parse} 184 * methods will throw {@link OptionsParsingException} unless 185 * {@link #getResidue()} is empty after parsing. 186 */ 187 public void setAllowResidue(boolean allowResidue) { 188 this.allowResidue = allowResidue; 189 } 190 191 /** 192 * Indicates whether or not the parser will allow long options with a 193 * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example. 194 */ 195 public void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) { 196 this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions); 197 } 198 199 /** Enables the Parser to handle params files loacted insinde the provided {@link FileSystem}. */ 200 public void enableParamsFileSupport(FileSystem fs) { 201 this.impl.setArgsPreProcessor(new ParamsFilePreProcessor(fs)); 202 } 203 204 public void parseAndExitUponError(String[] args) { 205 parseAndExitUponError(OptionPriority.COMMAND_LINE, "unknown", args); 206 } 207 208 /** 209 * A convenience function for use in main methods. Parses the command line 210 * parameters, and exits upon error. Also, prints out the usage message 211 * if "--help" appears anywhere within {@code args}. 212 */ 213 public void parseAndExitUponError(OptionPriority priority, String source, String[] args) { 214 for (String arg : args) { 215 if (arg.equals("--help")) { 216 System.out.println(describeOptions(Collections.<String, String>emptyMap(), 217 HelpVerbosity.LONG)); 218 System.exit(0); 219 } 220 } 221 try { 222 parse(priority, source, Arrays.asList(args)); 223 } catch (OptionsParsingException e) { 224 System.err.println("Error parsing command line: " + e.getMessage()); 225 System.err.println("Try --help."); 226 System.exit(2); 227 } 228 } 229 230 /** 231 * The metadata about an option. 232 */ 233 public static final class OptionDescription { 234 235 private final String name; 236 237 // For valued flags 238 private final Object defaultValue; 239 private final Converter<?> converter; 240 private final boolean allowMultiple; 241 242 private final ImmutableList<OptionValueDescription> expansions; 243 private final ImmutableList<OptionValueDescription> implicitRequirements; 244 245 OptionDescription( 246 String name, 247 Object defaultValue, 248 Converter<?> converter, 249 boolean allowMultiple, 250 ImmutableList<OptionValueDescription> expansions, 251 ImmutableList<OptionValueDescription> implicitRequirements) { 252 this.name = name; 253 this.defaultValue = defaultValue; 254 this.converter = converter; 255 this.allowMultiple = allowMultiple; 256 this.expansions = expansions; 257 this.implicitRequirements = implicitRequirements; 258 } 259 260 public String getName() { 261 return name; 262 } 263 264 public Object getDefaultValue() { 265 return defaultValue; 266 } 267 268 public Converter<?> getConverter() { 269 return converter; 270 } 271 272 public boolean getAllowMultiple() { 273 return allowMultiple; 274 } 275 276 public ImmutableList<OptionValueDescription> getImplicitRequirements() { 277 return implicitRequirements; 278 } 279 280 public ImmutableList<OptionValueDescription> getExpansions() { 281 return expansions; 282 } 283 } 284 285 /** 286 * The name and value of an option with additional metadata describing its 287 * priority, source, whether it was set via an implicit dependency, and if so, 288 * by which other option. 289 */ 290 public static class OptionValueDescription { 291 private final String name; 292 @Nullable private final String originalValueString; 293 @Nullable private final Object value; 294 @Nullable private final OptionPriority priority; 295 @Nullable private final String source; 296 @Nullable private final String implicitDependant; 297 @Nullable private final String expandedFrom; 298 private final boolean allowMultiple; 299 300 public OptionValueDescription( 301 String name, 302 @Nullable String originalValueString, 303 @Nullable Object value, 304 @Nullable OptionPriority priority, 305 @Nullable String source, 306 @Nullable String implicitDependant, 307 @Nullable String expandedFrom, 308 boolean allowMultiple) { 309 this.name = name; 310 this.originalValueString = originalValueString; 311 this.value = value; 312 this.priority = priority; 313 this.source = source; 314 this.implicitDependant = implicitDependant; 315 this.expandedFrom = expandedFrom; 316 this.allowMultiple = allowMultiple; 317 } 318 319 public String getName() { 320 return name; 321 } 322 323 public String getOriginalValueString() { 324 return originalValueString; 325 } 326 327 // Need to suppress unchecked warnings, because the "multiple occurrence" 328 // options use unchecked ListMultimaps due to limitations of Java generics. 329 @SuppressWarnings({"unchecked", "rawtypes"}) 330 public Object getValue() { 331 if (allowMultiple) { 332 // Sort the results by option priority and return them in a new list. 333 // The generic type of the list is not known at runtime, so we can't 334 // use it here. It was already checked in the constructor, so this is 335 // type-safe. 336 List result = new ArrayList<>(); 337 ListMultimap realValue = (ListMultimap) value; 338 for (OptionPriority priority : OptionPriority.values()) { 339 // If there is no mapping for this key, this check avoids object creation (because 340 // ListMultimap has to return a new object on get) and also an unnecessary addAll call. 341 if (realValue.containsKey(priority)) { 342 result.addAll(realValue.get(priority)); 343 } 344 } 345 return result; 346 } 347 return value; 348 } 349 350 /** 351 * @return the priority of the thing that set this value for this flag 352 */ 353 public OptionPriority getPriority() { 354 return priority; 355 } 356 357 /** 358 * @return the thing that set this value for this flag 359 */ 360 public String getSource() { 361 return source; 362 } 363 364 public String getImplicitDependant() { 365 return implicitDependant; 366 } 367 368 public boolean isImplicitDependency() { 369 return implicitDependant != null; 370 } 371 372 public String getExpansionParent() { 373 return expandedFrom; 374 } 375 376 public boolean isExpansion() { 377 return expandedFrom != null; 378 } 379 380 public boolean getAllowMultiple() { 381 return allowMultiple; 382 } 383 384 @Override 385 public String toString() { 386 StringBuilder result = new StringBuilder(); 387 result.append("option '").append(name).append("' "); 388 result.append("set to '").append(value).append("' "); 389 result.append("with priority ").append(priority); 390 if (source != null) { 391 result.append(" and source '").append(source).append("'"); 392 } 393 if (implicitDependant != null) { 394 result.append(" implicitly by "); 395 } 396 return result.toString(); 397 } 398 399 // Need to suppress unchecked warnings, because the "multiple occurrence" 400 // options use unchecked ListMultimaps due to limitations of Java generics. 401 @SuppressWarnings({"unchecked", "rawtypes"}) 402 void addValue(OptionPriority addedPriority, Object addedValue) { 403 Preconditions.checkState(allowMultiple); 404 ListMultimap optionValueList = (ListMultimap) value; 405 if (addedValue instanceof List<?>) { 406 optionValueList.putAll(addedPriority, (List<?>) addedValue); 407 } else { 408 optionValueList.put(addedPriority, addedValue); 409 } 410 } 411 } 412 413 /** 414 * The name and unparsed value of an option with additional metadata describing its 415 * priority, source, whether it was set via an implicit dependency, and if so, 416 * by which other option. 417 * 418 * <p>Note that the unparsed value and the source parameters can both be null. 419 */ 420 public static class UnparsedOptionValueDescription { 421 private final String name; 422 private final Field field; 423 private final String unparsedValue; 424 private final OptionPriority priority; 425 private final String source; 426 private final boolean explicit; 427 428 public UnparsedOptionValueDescription(String name, Field field, String unparsedValue, 429 OptionPriority priority, String source, boolean explicit) { 430 this.name = name; 431 this.field = field; 432 this.unparsedValue = unparsedValue; 433 this.priority = priority; 434 this.source = source; 435 this.explicit = explicit; 436 } 437 438 public String getName() { 439 return name; 440 } 441 442 Field getField() { 443 return field; 444 } 445 446 public boolean isBooleanOption() { 447 return field.getType().equals(boolean.class); 448 } 449 450 private OptionUsageRestrictions optionUsageRestrictions() { 451 return field.getAnnotation(Option.class).optionUsageRestrictions(); 452 } 453 454 public boolean isDocumented() { 455 return optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED; 456 } 457 458 public boolean isHidden() { 459 return optionUsageRestrictions() == OptionUsageRestrictions.HIDDEN 460 || optionUsageRestrictions() == OptionUsageRestrictions.INTERNAL; 461 } 462 463 boolean isExpansion() { 464 return OptionsData.isExpansionOption(field.getAnnotation(Option.class)); 465 } 466 467 boolean isImplicitRequirement() { 468 Option option = field.getAnnotation(Option.class); 469 return option.implicitRequirements().length > 0; 470 } 471 472 boolean allowMultiple() { 473 Option option = field.getAnnotation(Option.class); 474 return option.allowMultiple(); 475 } 476 477 public String getUnparsedValue() { 478 return unparsedValue; 479 } 480 481 OptionPriority getPriority() { 482 return priority; 483 } 484 485 public String getSource() { 486 return source; 487 } 488 489 public boolean isExplicit() { 490 return explicit; 491 } 492 493 @Override 494 public String toString() { 495 StringBuilder result = new StringBuilder(); 496 result.append("option '").append(name).append("' "); 497 result.append("set to '").append(unparsedValue).append("' "); 498 result.append("with priority ").append(priority); 499 if (source != null) { 500 result.append(" and source '").append(source).append("'"); 501 } 502 return result.toString(); 503 } 504 } 505 506 /** 507 * The verbosity with which option help messages are displayed: short (just 508 * the name), medium (name, type, default, abbreviation), and long (full 509 * description). 510 */ 511 public enum HelpVerbosity { LONG, MEDIUM, SHORT } 512 513 /** 514 * The restrictions on an option. Only documented options are output as part of the help and are 515 * intended for general user use. Undocumented options can be used by any user but aren't 516 * advertised and in practice should be used by bazel developers or early adopters helping to test 517 * a feature. 518 * 519 * <p>We use HIDDEN so that options that form the protocol between the client and the server are 520 * not logged. These are flags, but should never be set by a user. 521 * 522 * <p>Options which are INTERNAL are not recognized by the parser at all, and so cannot be used as 523 * flags. 524 */ 525 public enum OptionUsageRestrictions { 526 DOCUMENTED, UNDOCUMENTED, HIDDEN, INTERNAL 527 } 528 529 /** 530 * Returns a description of all the options this parser can digest. In addition to {@link Option} 531 * annotations, this method also interprets {@link OptionsUsage} annotations which give an 532 * intuitive short description for the options. Options of the same category (see {@link 533 * Option#category}) will be grouped together. 534 * 535 * @param categoryDescriptions a mapping from category names to category descriptions. 536 * Descriptions are optional; if omitted, a string based on the category name will be used. 537 * @param helpVerbosity if {@code long}, the options will be described verbosely, including their 538 * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if 539 * {@code short}, the options are just enumerated. 540 */ 541 public String describeOptions( 542 Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) { 543 OptionsData data = impl.getOptionsData(); 544 StringBuilder desc = new StringBuilder(); 545 if (!data.getOptionsClasses().isEmpty()) { 546 List<Field> allFields = new ArrayList<>(); 547 for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { 548 allFields.addAll(data.getFieldsForClass(optionsClass)); 549 } 550 Collections.sort(allFields, OptionsUsage.BY_CATEGORY); 551 String prevCategory = null; 552 553 for (Field optionField : allFields) { 554 Option option = optionField.getAnnotation(Option.class); 555 String category = option.category(); 556 if (!category.equals(prevCategory) 557 && option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { 558 String description = categoryDescriptions.get(category); 559 if (description == null) { 560 description = "Options category '" + category + "'"; 561 } 562 desc.append("\n").append(description).append(":\n"); 563 prevCategory = category; 564 } 565 566 if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { 567 OptionsUsage.getUsage(optionField, desc, helpVerbosity, impl.getOptionsData()); 568 } 569 } 570 } 571 return desc.toString().trim(); 572 } 573 574 /** 575 * Returns a description of all the options this parser can digest. 576 * In addition to {@link Option} annotations, this method also 577 * interprets {@link OptionsUsage} annotations which give an intuitive short 578 * description for the options. 579 * 580 * @param categoryDescriptions a mapping from category names to category 581 * descriptions. Options of the same category (see {@link 582 * Option#category}) will be grouped together, preceded by the description 583 * of the category. 584 */ 585 public String describeOptionsHtml(Map<String, String> categoryDescriptions, Escaper escaper) { 586 OptionsData data = impl.getOptionsData(); 587 StringBuilder desc = new StringBuilder(); 588 if (!data.getOptionsClasses().isEmpty()) { 589 List<Field> allFields = new ArrayList<>(); 590 for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { 591 allFields.addAll(data.getFieldsForClass(optionsClass)); 592 } 593 Collections.sort(allFields, OptionsUsage.BY_CATEGORY); 594 String prevCategory = null; 595 596 for (Field optionField : allFields) { 597 Option option = optionField.getAnnotation(Option.class); 598 String category = option.category(); 599 if (!category.equals(prevCategory) 600 && option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { 601 String description = categoryDescriptions.get(category); 602 if (description == null) { 603 description = "Options category '" + category + "'"; 604 } 605 if (prevCategory != null) { 606 desc.append("</dl>\n\n"); 607 } 608 desc.append(escaper.escape(description)).append(":\n"); 609 desc.append("<dl>"); 610 prevCategory = category; 611 } 612 613 if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { 614 OptionsUsage.getUsageHtml(optionField, desc, escaper, impl.getOptionsData()); 615 } 616 } 617 desc.append("</dl>\n"); 618 } 619 return desc.toString(); 620 } 621 622 /** 623 * Returns a string listing the possible flag completion for this command along with the command 624 * completion if any. See {@link OptionsUsage#getCompletion(Field, StringBuilder)} for more 625 * details on the format for the flag completion. 626 */ 627 public String getOptionsCompletion() { 628 OptionsData data = impl.getOptionsData(); 629 StringBuilder desc = new StringBuilder(); 630 631 // List all options 632 List<Field> allFields = new ArrayList<>(); 633 for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { 634 allFields.addAll(data.getFieldsForClass(optionsClass)); 635 } 636 // Sort field for deterministic ordering 637 Collections.sort(allFields, new Comparator<Field>() { 638 @Override 639 public int compare(Field f1, Field f2) { 640 String name1 = f1.getAnnotation(Option.class).name(); 641 String name2 = f2.getAnnotation(Option.class).name(); 642 return name1.compareTo(name2); 643 } 644 }); 645 for (Field optionField : allFields) { 646 Option option = optionField.getAnnotation(Option.class); 647 if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { 648 OptionsUsage.getCompletion(optionField, desc); 649 } 650 } 651 652 return desc.toString(); 653 } 654 655 /** 656 * Returns a description of the option. 657 * 658 * @return The {@link OptionDescription} for the option, or null if there is no option by the 659 * given name. 660 */ 661 public OptionDescription getOptionDescription(String name) throws OptionsParsingException { 662 return impl.getOptionDescription(name); 663 } 664 665 /** 666 * Returns a description of the option value set by the last previous call to 667 * {@link #parse(OptionPriority, String, List)} that successfully set the given 668 * option. If the option is of type {@link List}, the description will 669 * correspond to any one of the calls, but not necessarily the last. 670 * 671 * @return The {@link OptionValueDescription} for the option, or null if the value has not been 672 * set. 673 * @throws IllegalArgumentException if there is no option by the given name. 674 */ 675 public OptionValueDescription getOptionValueDescription(String name) { 676 return impl.getOptionValueDescription(name); 677 } 678 679 /** 680 * A convenience method, equivalent to 681 * {@code parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args))}. 682 */ 683 public void parse(String... args) throws OptionsParsingException { 684 parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args)); 685 } 686 687 /** 688 * A convenience method, equivalent to 689 * {@code parse(OptionPriority.COMMAND_LINE, null, args)}. 690 */ 691 public void parse(List<String> args) throws OptionsParsingException { 692 parse(OptionPriority.COMMAND_LINE, null, args); 693 } 694 695 /** 696 * Parses {@code args}, using the classes registered with this parser. 697 * {@link #getOptions(Class)} and {@link #getResidue()} return the results. 698 * May be called multiple times; later options override existing ones if they 699 * have equal or higher priority. The source of options is a free-form string 700 * that can be used for debugging. Strings that cannot be parsed as options 701 * accumulates as residue, if this parser allows it. 702 * 703 * @see OptionPriority 704 */ 705 public void parse(OptionPriority priority, String source, 706 List<String> args) throws OptionsParsingException { 707 parseWithSourceFunction(priority, Functions.constant(source), args); 708 } 709 710 /** 711 * Parses {@code args}, using the classes registered with this parser. 712 * {@link #getOptions(Class)} and {@link #getResidue()} return the results. May be called 713 * multiple times; later options override existing ones if they have equal or higher priority. 714 * The source of options is given as a function that maps option names to the source of the 715 * option. Strings that cannot be parsed as options accumulates as* residue, if this parser 716 * allows it. 717 */ 718 public void parseWithSourceFunction(OptionPriority priority, 719 Function<? super String, String> sourceFunction, List<String> args) 720 throws OptionsParsingException { 721 Preconditions.checkNotNull(priority); 722 Preconditions.checkArgument(priority != OptionPriority.DEFAULT); 723 residue.addAll(impl.parse(priority, sourceFunction, args)); 724 if (!allowResidue && !residue.isEmpty()) { 725 String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); 726 throw new OptionsParsingException(errorMsg); 727 } 728 } 729 730 /** 731 * Clears the given option. 732 * 733 * <p>This will not affect options objects that have already been retrieved from this parser 734 * through {@link #getOptions(Class)}. 735 * 736 * @param optionName The full name of the option to clear. 737 * @return A map of an option name to the old value of the options that were cleared. 738 * @throws IllegalArgumentException If the flag does not exist. 739 */ 740 public OptionValueDescription clearValue(String optionName) 741 throws OptionsParsingException { 742 OptionValueDescription clearedValue = impl.clearValue(optionName); 743 return clearedValue; 744 } 745 746 @Override 747 public List<String> getResidue() { 748 return ImmutableList.copyOf(residue); 749 } 750 751 /** 752 * Returns a list of warnings about problems encountered by previous parse calls. 753 */ 754 public List<String> getWarnings() { 755 return impl.getWarnings(); 756 } 757 758 @Override 759 public <O extends OptionsBase> O getOptions(Class<O> optionsClass) { 760 return impl.getParsedOptions(optionsClass); 761 } 762 763 @Override 764 public boolean containsExplicitOption(String name) { 765 return impl.containsExplicitOption(name); 766 } 767 768 @Override 769 public List<UnparsedOptionValueDescription> asListOfUnparsedOptions() { 770 return impl.asListOfUnparsedOptions(); 771 } 772 773 @Override 774 public List<UnparsedOptionValueDescription> asListOfExplicitOptions() { 775 return impl.asListOfExplicitOptions(); 776 } 777 778 @Override 779 public List<OptionValueDescription> asListOfEffectiveOptions() { 780 return impl.asListOfEffectiveOptions(); 781 } 782 783 @Override 784 public List<String> canonicalize() { 785 return impl.asCanonicalizedList(); 786 } 787 788 /** Returns all options fields of the given options class, in alphabetic order. */ 789 public static Collection<Field> getFields(Class<? extends OptionsBase> optionsClass) { 790 OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); 791 return data.getFieldsForClass(optionsClass); 792 } 793 794 /** 795 * Returns whether the given options class uses only the core types listed in {@link 796 * OptionsBase#coreTypes}. These are guaranteed to be deeply immutable and serializable. 797 */ 798 public static boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) { 799 OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); 800 return data.getUsesOnlyCoreTypes(optionsClass); 801 } 802 803 /** 804 * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited 805 * ones) to its value in {@code options}. 806 * 807 * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa. 808 * The map entries appear sorted alphabetically by option name. 809 * 810 * If {@code options} is an instance of a subclass of {@code optionsClass}, any options defined 811 * by the subclass are not included in the map. 812 * 813 * @throws IllegalArgumentException if {@code options} is not an instance of {@code optionsClass} 814 */ 815 public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) { 816 OptionsData data = getOptionsDataInternal(optionsClass); 817 // Alphabetized due to getFieldsForClass()'s order. 818 Map<Field, Object> map = new LinkedHashMap<>(); 819 for (Field field : data.getFieldsForClass(optionsClass)) { 820 try { 821 map.put(field, field.get(options)); 822 } catch (IllegalAccessException e) { 823 // All options fields of options classes should be public. 824 throw new IllegalStateException(e); 825 } catch (IllegalArgumentException e) { 826 // This would indicate an inconsistency in the cached OptionsData. 827 throw new IllegalStateException(e); 828 } 829 } 830 return map; 831 } 832 833 /** 834 * Given a mapping as returned by {@link #toMap}, and the options class it that its entries 835 * correspond to, this constructs the corresponding instance of the options class. 836 * 837 * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code 838 * optionsClass}, with values of the appropriate type 839 */ 840 public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) { 841 // Instantiate the options class. 842 OptionsData data = getOptionsDataInternal(optionsClass); 843 O optionsInstance; 844 try { 845 Constructor<O> constructor = data.getConstructor(optionsClass); 846 Preconditions.checkNotNull(constructor, "No options class constructor available"); 847 optionsInstance = constructor.newInstance(); 848 } catch (ReflectiveOperationException e) { 849 throw new IllegalStateException("Error while instantiating options class", e); 850 } 851 852 List<Field> fields = data.getFieldsForClass(optionsClass); 853 // Ensure all fields are covered, no extraneous fields. 854 validateFieldsSets(new LinkedHashSet<>(fields), new LinkedHashSet<>(map.keySet())); 855 // Populate the instance. 856 for (Field field : fields) { 857 // Non-null as per above check. 858 Object value = map.get(field); 859 try { 860 field.set(optionsInstance, value); 861 } catch (IllegalAccessException e) { 862 throw new IllegalStateException(e); 863 } 864 // May also throw IllegalArgumentException if map value is ill typed. 865 } 866 return optionsInstance; 867 } 868 869 /** 870 * Raises a pretty {@link IllegalArgumentException} if the two sets of fields are not equal. 871 * 872 * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link 873 * Option} annotation. (This isn't done for {@code fieldsFromClass} because they come from an 874 * {@link OptionsData} object.) 875 */ 876 private static void validateFieldsSets( 877 LinkedHashSet<Field> fieldsFromClass, LinkedHashSet<Field> fieldsFromMap) { 878 if (!fieldsFromClass.equals(fieldsFromMap)) { 879 List<String> extraNamesFromClass = new ArrayList<>(); 880 List<String> extraNamesFromMap = new ArrayList<>(); 881 for (Field field : fieldsFromClass) { 882 if (!fieldsFromMap.contains(field)) { 883 extraNamesFromClass.add("'" + field.getAnnotation(Option.class).name() + "'"); 884 } 885 } 886 for (Field field : fieldsFromMap) { 887 // Extra validation on the map keys since they don't come from OptionsData. 888 if (!fieldsFromClass.contains(field)) { 889 if (field == null) { 890 extraNamesFromMap.add("<null field>"); 891 } else { 892 Option annotation = field.getAnnotation(Option.class); 893 if (annotation == null) { 894 extraNamesFromMap.add("<non-Option field>"); 895 } else { 896 extraNamesFromMap.add("'" + annotation.name() + "'"); 897 } 898 } 899 } 900 } 901 throw new IllegalArgumentException( 902 "Map keys do not match fields of options class; extra map keys: {" 903 + Joiner.on(", ").join(extraNamesFromMap) + "}; extra options class options: {" 904 + Joiner.on(", ").join(extraNamesFromClass) + "}"); 905 } 906 } 907 } 908