• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&lt;String&gt; 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