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
15package com.google.devtools.common.options;
16
17import com.google.common.base.Joiner;
18import com.google.common.base.Preconditions;
19import com.google.common.base.Throwables;
20import com.google.common.collect.ArrayListMultimap;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.ImmutableMap;
23import com.google.common.collect.ListMultimap;
24import com.google.common.escape.Escaper;
25import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
26import java.lang.reflect.Constructor;
27import java.lang.reflect.Field;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.LinkedHashMap;
33import java.util.LinkedHashSet;
34import java.util.List;
35import java.util.Map;
36import java.util.Map.Entry;
37import java.util.Set;
38import java.util.function.Consumer;
39import java.util.function.Function;
40import java.util.function.Predicate;
41import 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 */
70public 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 {
83    public ConstructionException(String message) {
84      super(message);
85    }
86
87    public ConstructionException(Throwable cause) {
88      super(cause);
89    }
90
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   */
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   */
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   */
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   */
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) */
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}. */
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   */
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
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   */
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   */
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   */
206  public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) {
207    this.impl.setArgsPreProcessor(preProcessor);
208  }
209
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   */
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
243    OptionDescription(OptionDefinition definition, OptionsData optionsData) {
244      this.optionDefinition = definition;
245      this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition);
246    }
247
248    public OptionDefinition getOptionDefinition() {
249      return optionDefinition;
250    }
251
252    public boolean isExpansion() {
253      return optionDefinition.isExpansionOption();
254    }
255
256    /** Return a list of flags that this option expands to. */
257    public ImmutableList<String> getExpansion() throws OptionsParsingException {
258      return evaluatedExpansion;
259    }
260
261    @Override
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
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   */
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>>
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
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
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   */
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   */
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
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   */
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   */
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   */
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   */
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   */
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   */
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   */
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
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   */
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   */
659  public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException {
660    return impl.clearValue(option);
661  }
662
663  @Override
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. */
669  public List<String> getWarnings() {
670    return impl.getWarnings();
671  }
672
673  @Override
674  public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
675    return impl.getParsedOptions(optionsClass);
676  }
677
678  @Override
679  public boolean containsExplicitOption(String name) {
680    return impl.containsExplicitOption(name);
681  }
682
683  @Override
684  public List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
685    return impl.asCompleteListOfParsedOptions();
686  }
687
688  @Override
689  public List<ParsedOptionDescription> asListOfExplicitOptions() {
690    return impl.asListOfExplicitOptions();
691  }
692
693  @Override
694  public List<ParsedOptionDescription> asListOfCanonicalOptions() {
695    return impl.asCanonicalizedListOfParsedOptions();
696  }
697
698  @Override
699  public List<OptionValueDescription> asListOfOptionValues() {
700    return impl.asListOfEffectiveOptions();
701  }
702
703  @Override
704  public List<String> canonicalize() {
705    return impl.asCanonicalizedList();
706  }
707
708  /** Returns all options fields of the given options class, in alphabetic order. */
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   */
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   */
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   */
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   */
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