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