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&lt;String&gt; 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 {
ConstructionException(String message)82     public ConstructionException(String message) {
83       super(message);
84     }
85 
ConstructionException(Throwable cause)86     public ConstructionException(Throwable cause) {
87       super(cause);
88     }
89 
ConstructionException(String message, Throwable cause)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    */
getOptionsData( List<Class<? extends OptionsBase>> optionsClasses)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    */
getOptionsDataInternal( List<Class<? extends OptionsBase>> optionsClasses)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    */
getOptionsDataInternal(Class<? extends OptionsBase> optionsClass)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    */
newOptionsParser(Class<? extends OptionsBase> class1)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) */
newOptionsParser( Class<? extends OptionsBase> class1, Class<? extends OptionsBase> class2)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}. */
newOptionsParser( Iterable<? extends Class<? extends OptionsBase>> optionsClasses)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    */
newOptionsParser(OpaqueOptionsData optionsData)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 
OptionsParser(OptionsData optionsData)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    */
setAllowResidue(boolean allowResidue)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    */
setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions)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    */
enableParamsFileSupport(ParamsFilePreProcessor preProcessor)205   public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) {
206     this.impl.setArgsPreProcessor(preProcessor);
207   }
208 
parseAndExitUponError(String[] args)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    */
parseAndExitUponError( OptionPriority.PriorityCategory priority, String source, String[] args)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 
OptionDescription(OptionDefinition definition, OptionsData optionsData)242     OptionDescription(OptionDefinition definition, OptionsData optionsData) {
243       this.optionDefinition = definition;
244       this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition);
245     }
246 
getOptionDefinition()247     public OptionDefinition getOptionDefinition() {
248       return optionDefinition;
249     }
250 
isExpansion()251     public boolean isExpansion() {
252       return optionDefinition.isExpansionOption();
253     }
254 
255     /** Return a list of flags that this option expands to. */
getExpansion()256     public ImmutableList<String> getExpansion() throws OptionsParsingException {
257       return evaluatedExpansion;
258     }
259 
260     @Override
equals(Object obj)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
hashCode()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    */
describeOptions(String productName, HelpVerbosity helpVerbosity)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>>
getOptionsSortedByCategory()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
describeOptionsWithDeprecatedCategories( Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity)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
describeOptionsHtmlWithDeprecatedCategories( Map<String, String> categoryDescriptions, Escaper escaper)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    */
describeOptionsHtml(Escaper escaper, String productName)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    */
getOptionsCompletion()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 
visitOptions( Predicate<OptionDefinition> predicate, Consumer<OptionDefinition> visitor)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    */
getOptionDescription(String name)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    */
getExpansionValueDescriptions( OptionDefinition expansionOption, OptionInstanceOrigin originOfExpansionOption)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    */
getOptionValueDescription(String name)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    */
parse(String... args)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    */
parse(List<String> args)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    */
parse(OptionPriority.PriorityCategory priority, String source, List<String> args)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    */
parseWithSourceFunction( OptionPriority.PriorityCategory priority, Function<OptionDefinition, String> sourceFunction, List<String> args)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    */
parseArgsAsExpansionOfOption( ParsedOptionDescription optionToExpand, String source, List<String> args)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    */
addOptionValueAtSpecificPriority( OptionInstanceOrigin origin, OptionDefinition option, String value)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    */
clearValue(OptionDefinition option)671   public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException {
672     return impl.clearValue(option);
673   }
674 
675   @Override
getResidue()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. */
getWarnings()681   public List<String> getWarnings() {
682     return impl.getWarnings();
683   }
684 
685   @Override
getOptions(Class<O> optionsClass)686   public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
687     return impl.getParsedOptions(optionsClass);
688   }
689 
690   @Override
containsExplicitOption(String name)691   public boolean containsExplicitOption(String name) {
692     return impl.containsExplicitOption(name);
693   }
694 
695   @Override
asCompleteListOfParsedOptions()696   public List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
697     return impl.asCompleteListOfParsedOptions();
698   }
699 
700   @Override
asListOfExplicitOptions()701   public List<ParsedOptionDescription> asListOfExplicitOptions() {
702     return impl.asListOfExplicitOptions();
703   }
704 
705   @Override
asListOfCanonicalOptions()706   public List<ParsedOptionDescription> asListOfCanonicalOptions() {
707     return impl.asCanonicalizedListOfParsedOptions();
708   }
709 
710   @Override
asListOfOptionValues()711   public List<OptionValueDescription> asListOfOptionValues() {
712     return impl.asListOfEffectiveOptions();
713   }
714 
715   @Override
canonicalize()716   public List<String> canonicalize() {
717     return impl.asCanonicalizedList();
718   }
719 
720   /** Returns all options fields of the given options class, in alphabetic order. */
getOptionDefinitions( Class<? extends OptionsBase> optionsClass)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    */
getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass)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    */
toMap(Class<O> optionsClass, O options)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    */
fromMap(Class<O> optionsClass, Map<Field, Object> map)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    */
validateFieldsSets( Class<? extends OptionsBase> optionsClass, LinkedHashSet<Field> fieldsFromMap)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