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