| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.common.options; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.base.Throwables; | |
| import com.google.common.collect.ArrayListMultimap; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ListMultimap; | |
| import com.google.common.escape.Escaper; | |
| import com.google.devtools.common.options.OptionDefinition.NotAnOptionException; | |
| import java.lang.reflect.Constructor; | |
| import java.lang.reflect.Field; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.LinkedHashMap; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.function.Consumer; | |
| import java.util.function.Function; | |
| import java.util.function.Predicate; | |
| import java.util.stream.Collectors; | |
| /** | |
| * A parser for options. Typical use case in a main method: | |
| * | |
| * <pre> | |
| * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class, BarOptions.class); | |
| * parser.parseAndExitUponError(args); | |
| * FooOptions foo = parser.getOptions(FooOptions.class); | |
| * BarOptions bar = parser.getOptions(BarOptions.class); | |
| * List<String> otherArguments = parser.getResidue(); | |
| * </pre> | |
| * | |
| * <p>FooOptions and BarOptions would be options specification classes, derived from OptionsBase, | |
| * that contain fields annotated with @Option(...). | |
| * | |
| * <p>Alternatively, rather than calling {@link | |
| * #parseAndExitUponError(OptionPriority.PriorityCategory, String, String[])}, client code may call | |
| * {@link #parse(OptionPriority.PriorityCategory,String,List)}, and handle parser exceptions usage | |
| * messages themselves. | |
| * | |
| * <p>This options parsing implementation has (at least) one design flaw. It allows both '--foo=baz' | |
| * and '--foo baz' for all options except void, boolean and tristate options. For these, the 'baz' | |
| * in '--foo baz' is not treated as a parameter to the option, making it is impossible to switch | |
| * options between void/boolean/tristate and everything else without breaking backwards | |
| * compatibility. | |
| * | |
| * @see Options a simpler class which you can use if you only have one options specification class | |
| */ | |
| public class OptionsParser implements OptionsProvider { | |
| // TODO(b/65049598) make ConstructionException checked. | |
| /** | |
| * An unchecked exception thrown when there is a problem constructing a parser, e.g. an error | |
| * while validating an {@link OptionDefinition} in one of its {@link OptionsBase} subclasses. | |
| * | |
| * <p>This exception is unchecked because it generally indicates an internal error affecting all | |
| * invocations of the program. I.e., any such error should be immediately obvious to the | |
| * developer. Although unchecked, we explicitly mark some methods as throwing it as a reminder in | |
| * the API. | |
| */ | |
| public static class ConstructionException extends RuntimeException { | |
| public ConstructionException(String message) { | |
| super(message); | |
| } | |
| public ConstructionException(Throwable cause) { | |
| super(cause); | |
| } | |
| public ConstructionException(String message, Throwable cause) { | |
| super(message, cause); | |
| } | |
| } | |
| /** | |
| * A cache for the parsed options data. Both keys and values are immutable, so | |
| * this is always safe. Only access this field through the {@link | |
| * #getOptionsData} method for thread-safety! The cache is very unlikely to | |
| * grow to a significant amount of memory, because there's only a fixed set of | |
| * options classes on the classpath. | |
| */ | |
| private static final Map<ImmutableList<Class<? extends OptionsBase>>, OptionsData> optionsData = | |
| new HashMap<>(); | |
| /** | |
| * Returns {@link OpaqueOptionsData} suitable for passing along to {@link | |
| * #newOptionsParser(OpaqueOptionsData optionsData)}. | |
| * | |
| * <p>This is useful when you want to do the work of analyzing the given {@code optionsClasses} | |
| * exactly once, but you want to parse lots of different lists of strings (and thus need to | |
| * construct lots of different {@link OptionsParser} instances). | |
| */ | |
| public static OpaqueOptionsData getOptionsData( | |
| List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { | |
| return getOptionsDataInternal(optionsClasses); | |
| } | |
| /** | |
| * Returns the {@link OptionsData} associated with the given list of options classes. | |
| */ | |
| static synchronized OptionsData getOptionsDataInternal( | |
| List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException { | |
| ImmutableList<Class<? extends OptionsBase>> immutableOptionsClasses = | |
| ImmutableList.copyOf(optionsClasses); | |
| OptionsData result = optionsData.get(immutableOptionsClasses); | |
| if (result == null) { | |
| try { | |
| result = OptionsData.from(immutableOptionsClasses); | |
| } catch (Exception e) { | |
| Throwables.throwIfInstanceOf(e, ConstructionException.class); | |
| throw new ConstructionException(e.getMessage(), e); | |
| } | |
| optionsData.put(immutableOptionsClasses, result); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Returns the {@link OptionsData} associated with the given options class. | |
| */ | |
| static OptionsData getOptionsDataInternal(Class<? extends OptionsBase> optionsClass) | |
| throws ConstructionException { | |
| return getOptionsDataInternal(ImmutableList.of(optionsClass)); | |
| } | |
| /** | |
| * @see #newOptionsParser(Iterable) | |
| */ | |
| public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1) | |
| throws ConstructionException { | |
| return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1)); | |
| } | |
| /** @see #newOptionsParser(Iterable) */ | |
| public static OptionsParser newOptionsParser( | |
| Class<? extends OptionsBase> class1, Class<? extends OptionsBase> class2) | |
| throws ConstructionException { | |
| return newOptionsParser(ImmutableList.of(class1, class2)); | |
| } | |
| /** Create a new {@link OptionsParser}. */ | |
| public static OptionsParser newOptionsParser( | |
| Iterable<? extends Class<? extends OptionsBase>> optionsClasses) | |
| throws ConstructionException { | |
| return newOptionsParser(getOptionsDataInternal(ImmutableList.copyOf(optionsClasses))); | |
| } | |
| /** | |
| * Create a new {@link OptionsParser}, using {@link OpaqueOptionsData} previously returned from | |
| * {@link #getOptionsData}. | |
| */ | |
| public static OptionsParser newOptionsParser(OpaqueOptionsData optionsData) { | |
| return new OptionsParser((OptionsData) optionsData); | |
| } | |
| private final OptionsParserImpl impl; | |
| private final List<String> residue = new ArrayList<String>(); | |
| private boolean allowResidue = true; | |
| OptionsParser(OptionsData optionsData) { | |
| impl = new OptionsParserImpl(optionsData); | |
| } | |
| /** | |
| * Indicates whether or not the parser will allow a non-empty residue; that | |
| * is, iff this value is true then a call to one of the {@code parse} | |
| * methods will throw {@link OptionsParsingException} unless | |
| * {@link #getResidue()} is empty after parsing. | |
| */ | |
| public void setAllowResidue(boolean allowResidue) { | |
| this.allowResidue = allowResidue; | |
| } | |
| /** | |
| * Indicates whether or not the parser will allow long options with a | |
| * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example. | |
| */ | |
| public void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) { | |
| this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions); | |
| } | |
| /** | |
| * Enables the Parser to handle params files using the provided {@link ParamsFilePreProcessor}. | |
| */ | |
| public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) { | |
| this.impl.setArgsPreProcessor(preProcessor); | |
| } | |
| public void parseAndExitUponError(String[] args) { | |
| parseAndExitUponError(OptionPriority.PriorityCategory.COMMAND_LINE, "unknown", args); | |
| } | |
| /** | |
| * A convenience function for use in main methods. Parses the command line parameters, and exits | |
| * upon error. Also, prints out the usage message if "--help" appears anywhere within {@code | |
| * args}. | |
| */ | |
| public void parseAndExitUponError( | |
| OptionPriority.PriorityCategory priority, String source, String[] args) { | |
| for (String arg : args) { | |
| if (arg.equals("--help")) { | |
| System.out.println( | |
| describeOptionsWithDeprecatedCategories(ImmutableMap.of(), HelpVerbosity.LONG)); | |
| System.exit(0); | |
| } | |
| } | |
| try { | |
| parse(priority, source, Arrays.asList(args)); | |
| } catch (OptionsParsingException e) { | |
| System.err.println("Error parsing command line: " + e.getMessage()); | |
| System.err.println("Try --help."); | |
| System.exit(2); | |
| } | |
| } | |
| /** The metadata about an option, in the context of this options parser. */ | |
| public static final class OptionDescription { | |
| private final OptionDefinition optionDefinition; | |
| private final ImmutableList<String> evaluatedExpansion; | |
| OptionDescription(OptionDefinition definition, OptionsData optionsData) { | |
| this.optionDefinition = definition; | |
| this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition); | |
| } | |
| public OptionDefinition getOptionDefinition() { | |
| return optionDefinition; | |
| } | |
| public boolean isExpansion() { | |
| return optionDefinition.isExpansionOption(); | |
| } | |
| /** Return a list of flags that this option expands to. */ | |
| public ImmutableList<String> getExpansion() throws OptionsParsingException { | |
| return evaluatedExpansion; | |
| } | |
| @Override | |
| public boolean equals(Object obj) { | |
| if (obj instanceof OptionDescription) { | |
| OptionDescription other = (OptionDescription) obj; | |
| // Check that the option is the same, with the same expansion. | |
| return other.optionDefinition.equals(optionDefinition) | |
| && other.evaluatedExpansion.equals(evaluatedExpansion); | |
| } | |
| return false; | |
| } | |
| @Override | |
| public int hashCode() { | |
| return optionDefinition.hashCode() + evaluatedExpansion.hashCode(); | |
| } | |
| } | |
| /** | |
| * The verbosity with which option help messages are displayed: short (just | |
| * the name), medium (name, type, default, abbreviation), and long (full | |
| * description). | |
| */ | |
| public enum HelpVerbosity { LONG, MEDIUM, SHORT } | |
| /** | |
| * Returns a description of all the options this parser can digest. In addition to {@link Option} | |
| * annotations, this method also interprets {@link OptionsUsage} annotations which give an | |
| * intuitive short description for the options. Options of the same category (see {@link | |
| * OptionDocumentationCategory}) will be grouped together. | |
| * | |
| * @param productName the name of this product (blaze, bazel) | |
| * @param helpVerbosity if {@code long}, the options will be described verbosely, including their | |
| * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if | |
| * {@code short}, the options are just enumerated. | |
| */ | |
| public String describeOptions(String productName, HelpVerbosity helpVerbosity) { | |
| StringBuilder desc = new StringBuilder(); | |
| LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = | |
| getOptionsSortedByCategory(); | |
| ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = | |
| OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); | |
| for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e : | |
| optionsByCategory.entrySet()) { | |
| String categoryDescription = optionCategoryDescriptions.get(e.getKey()); | |
| List<OptionDefinition> categorizedOptionList = e.getValue(); | |
| // Describe the category if we're going to end up using it at all. | |
| if (!categorizedOptionList.isEmpty()) { | |
| desc.append("\n").append(categoryDescription).append(":\n"); | |
| } | |
| // Describe the options in this category. | |
| for (OptionDefinition optionDef : categorizedOptionList) { | |
| OptionsUsage.getUsage(optionDef, desc, helpVerbosity, impl.getOptionsData(), true); | |
| } | |
| } | |
| return desc.toString().trim(); | |
| } | |
| /** | |
| * @return all documented options loaded in this parser, grouped by categories in display order. | |
| */ | |
| private LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> | |
| getOptionsSortedByCategory() { | |
| OptionsData data = impl.getOptionsData(); | |
| if (data.getOptionsClasses().isEmpty()) { | |
| return new LinkedHashMap<>(); | |
| } | |
| // Get the documented options grouped by category. | |
| ListMultimap<OptionDocumentationCategory, OptionDefinition> optionsByCategories = | |
| ArrayListMultimap.create(); | |
| for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { | |
| for (OptionDefinition optionDefinition : | |
| OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { | |
| // Only track documented options. | |
| if (optionDefinition.getDocumentationCategory() | |
| != OptionDocumentationCategory.UNDOCUMENTED) { | |
| optionsByCategories.put(optionDefinition.getDocumentationCategory(), optionDefinition); | |
| } | |
| } | |
| } | |
| // Put the categories into display order and sort the options in each category. | |
| LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> sortedCategoriesToOptions = | |
| new LinkedHashMap<>(OptionFilterDescriptions.documentationOrder.length, 1); | |
| for (OptionDocumentationCategory category : OptionFilterDescriptions.documentationOrder) { | |
| List<OptionDefinition> optionList = optionsByCategories.get(category); | |
| if (optionList != null) { | |
| optionList.sort(OptionDefinition.BY_OPTION_NAME); | |
| sortedCategoriesToOptions.put(category, optionList); | |
| } | |
| } | |
| return sortedCategoriesToOptions; | |
| } | |
| /** | |
| * Returns a description of all the options this parser can digest. In addition to {@link Option} | |
| * annotations, this method also interprets {@link OptionsUsage} annotations which give an | |
| * intuitive short description for the options. Options of the same category (see {@link | |
| * Option#category}) will be grouped together. | |
| * | |
| * @param categoryDescriptions a mapping from category names to category descriptions. | |
| * Descriptions are optional; if omitted, a string based on the category name will be used. | |
| * @param helpVerbosity if {@code long}, the options will be described verbosely, including their | |
| * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if | |
| * {@code short}, the options are just enumerated. | |
| */ | |
| @Deprecated | |
| public String describeOptionsWithDeprecatedCategories( | |
| Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) { | |
| OptionsData data = impl.getOptionsData(); | |
| StringBuilder desc = new StringBuilder(); | |
| if (!data.getOptionsClasses().isEmpty()) { | |
| List<OptionDefinition> allFields = new ArrayList<>(); | |
| for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { | |
| allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); | |
| } | |
| Collections.sort(allFields, OptionDefinition.BY_CATEGORY); | |
| String prevCategory = null; | |
| for (OptionDefinition optionDefinition : allFields) { | |
| String category = optionDefinition.getOptionCategory(); | |
| if (!category.equals(prevCategory) | |
| && optionDefinition.getDocumentationCategory() | |
| != OptionDocumentationCategory.UNDOCUMENTED) { | |
| String description = categoryDescriptions.get(category); | |
| if (description == null) { | |
| description = "Options category '" + category + "'"; | |
| } | |
| desc.append("\n").append(description).append(":\n"); | |
| prevCategory = category; | |
| } | |
| if (optionDefinition.getDocumentationCategory() | |
| != OptionDocumentationCategory.UNDOCUMENTED) { | |
| OptionsUsage.getUsage( | |
| optionDefinition, desc, helpVerbosity, impl.getOptionsData(), false); | |
| } | |
| } | |
| } | |
| return desc.toString().trim(); | |
| } | |
| /** | |
| * Returns a description of all the options this parser can digest. In addition to {@link Option} | |
| * annotations, this method also interprets {@link OptionsUsage} annotations which give an | |
| * intuitive short description for the options. | |
| * | |
| * @param categoryDescriptions a mapping from category names to category descriptions. Options of | |
| * the same category (see {@link Option#category}) will be grouped together, preceded by the | |
| * description of the category. | |
| */ | |
| @Deprecated | |
| public String describeOptionsHtmlWithDeprecatedCategories( | |
| Map<String, String> categoryDescriptions, Escaper escaper) { | |
| OptionsData data = impl.getOptionsData(); | |
| StringBuilder desc = new StringBuilder(); | |
| if (!data.getOptionsClasses().isEmpty()) { | |
| List<OptionDefinition> allFields = new ArrayList<>(); | |
| for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { | |
| allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); | |
| } | |
| Collections.sort(allFields, OptionDefinition.BY_CATEGORY); | |
| String prevCategory = null; | |
| for (OptionDefinition optionDefinition : allFields) { | |
| String category = optionDefinition.getOptionCategory(); | |
| if (!category.equals(prevCategory) | |
| && optionDefinition.getDocumentationCategory() | |
| != OptionDocumentationCategory.UNDOCUMENTED) { | |
| String description = categoryDescriptions.get(category); | |
| if (description == null) { | |
| description = "Options category '" + category + "'"; | |
| } | |
| if (prevCategory != null) { | |
| desc.append("</dl>\n\n"); | |
| } | |
| desc.append(escaper.escape(description)).append(":\n"); | |
| desc.append("<dl>"); | |
| prevCategory = category; | |
| } | |
| if (optionDefinition.getDocumentationCategory() | |
| != OptionDocumentationCategory.UNDOCUMENTED) { | |
| OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData(), false); | |
| } | |
| } | |
| desc.append("</dl>\n"); | |
| } | |
| return desc.toString(); | |
| } | |
| /** | |
| * Returns a description of all the options this parser can digest. In addition to {@link Option} | |
| * annotations, this method also interprets {@link OptionsUsage} annotations which give an | |
| * intuitive short description for the options. | |
| */ | |
| public String describeOptionsHtml(Escaper escaper, String productName) { | |
| StringBuilder desc = new StringBuilder(); | |
| LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = | |
| getOptionsSortedByCategory(); | |
| ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = | |
| OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); | |
| for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e : | |
| optionsByCategory.entrySet()) { | |
| desc.append("<dl>"); | |
| String categoryDescription = optionCategoryDescriptions.get(e.getKey()); | |
| List<OptionDefinition> categorizedOptionsList = e.getValue(); | |
| // Describe the category if we're going to end up using it at all. | |
| if (!categorizedOptionsList.isEmpty()) { | |
| desc.append(escaper.escape(categoryDescription)).append(":\n"); | |
| } | |
| // Describe the options in this category. | |
| for (OptionDefinition optionDef : categorizedOptionsList) { | |
| OptionsUsage.getUsageHtml(optionDef, desc, escaper, impl.getOptionsData(), true); | |
| } | |
| desc.append("</dl>\n"); | |
| } | |
| return desc.toString(); | |
| } | |
| /** | |
| * Returns a string listing the possible flag completion for this command along with the command | |
| * completion if any. See {@link OptionsUsage#getCompletion(OptionDefinition, StringBuilder)} for | |
| * more details on the format for the flag completion. | |
| */ | |
| public String getOptionsCompletion() { | |
| StringBuilder desc = new StringBuilder(); | |
| visitOptions( | |
| optionDefinition -> | |
| optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED, | |
| optionDefinition -> OptionsUsage.getCompletion(optionDefinition, desc)); | |
| return desc.toString(); | |
| } | |
| public void visitOptions( | |
| Predicate<OptionDefinition> predicate, Consumer<OptionDefinition> visitor) { | |
| Preconditions.checkNotNull(predicate, "Missing predicate."); | |
| Preconditions.checkNotNull(visitor, "Missing visitor."); | |
| OptionsData data = impl.getOptionsData(); | |
| data.getOptionsClasses() | |
| // List all options | |
| .stream() | |
| .flatMap(optionsClass -> OptionsData.getAllOptionDefinitionsForClass(optionsClass).stream()) | |
| // Sort field for deterministic ordering | |
| .sorted(OptionDefinition.BY_OPTION_NAME) | |
| .filter(predicate) | |
| .forEach(visitor); | |
| } | |
| /** | |
| * Returns a description of the option. | |
| * | |
| * @return The {@link OptionDescription} for the option, or null if there is no option by the | |
| * given name. | |
| */ | |
| OptionDescription getOptionDescription(String name) throws OptionsParsingException { | |
| return impl.getOptionDescription(name); | |
| } | |
| /** | |
| * Returns the parsed options that get expanded from this option, whether it expands due to an | |
| * implicit requirement or expansion. | |
| * | |
| * @param expansionOption the option that might need to be expanded. If this option does not | |
| * expand to other options, the empty list will be returned. | |
| * @param originOfExpansionOption the origin of the option that's being expanded. This function | |
| * will take care of adjusting the source messages as necessary. | |
| */ | |
| ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions( | |
| OptionDefinition expansionOption, OptionInstanceOrigin originOfExpansionOption) | |
| throws OptionsParsingException { | |
| return impl.getExpansionValueDescriptions(expansionOption, originOfExpansionOption); | |
| } | |
| /** | |
| * Returns a description of the option value set by the last previous call to {@link | |
| * #parse(OptionPriority.PriorityCategory, String, List)} that successfully set the given option. | |
| * If the option is of type {@link List}, the description will correspond to any one of the calls, | |
| * but not necessarily the last. | |
| * | |
| * @return The {@link com.google.devtools.common.options.OptionValueDescription} for the option, | |
| * or null if the value has not been set. | |
| * @throws IllegalArgumentException if there is no option by the given name. | |
| */ | |
| public OptionValueDescription getOptionValueDescription(String name) { | |
| return impl.getOptionValueDescription(name); | |
| } | |
| /** | |
| * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, | |
| * Arrays.asList(args))}. | |
| */ | |
| public void parse(String... args) throws OptionsParsingException { | |
| parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList(args)); | |
| } | |
| /** | |
| * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, args)}. | |
| */ | |
| public void parse(List<String> args) throws OptionsParsingException { | |
| parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, args); | |
| } | |
| /** | |
| * Parses {@code args}, using the classes registered with this parser, at the given priority. | |
| * | |
| * <p>May be called multiple times; later options override existing ones if they have equal or | |
| * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this | |
| * parser allows it. | |
| * | |
| * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results. | |
| * | |
| * @param priority the priority at which to parse these options. Within this priority category, | |
| * each option will be given an index to track its position. If parse() has already been | |
| * called at this priority, the indexing will continue where it left off, to keep ordering. | |
| * @param source the source to track for each option parsed. | |
| * @param args the arg list to parse. Each element might be an option, a value linked to an | |
| * option, or residue. | |
| */ | |
| public void parse(OptionPriority.PriorityCategory priority, String source, List<String> args) | |
| throws OptionsParsingException { | |
| parseWithSourceFunction(priority, o -> source, args); | |
| } | |
| /** | |
| * Parses {@code args}, using the classes registered with this parser, at the given priority. | |
| * | |
| * <p>May be called multiple times; later options override existing ones if they have equal or | |
| * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this | |
| * parser allows it. | |
| * | |
| * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results. | |
| * | |
| * @param priority the priority at which to parse these options. Within this priority category, | |
| * each option will be given an index to track its position. If parse() has already been | |
| * called at this priority, the indexing will continue where it left off, to keep ordering. | |
| * @param sourceFunction a function that maps option names to the source of the option. | |
| * @param args the arg list to parse. Each element might be an option, a value linked to an | |
| * option, or residue. | |
| */ | |
| public void parseWithSourceFunction( | |
| OptionPriority.PriorityCategory priority, | |
| Function<OptionDefinition, String> sourceFunction, | |
| List<String> args) | |
| throws OptionsParsingException { | |
| Preconditions.checkNotNull(priority); | |
| Preconditions.checkArgument(priority != OptionPriority.PriorityCategory.DEFAULT); | |
| residue.addAll(impl.parse(priority, sourceFunction, args)); | |
| if (!allowResidue && !residue.isEmpty()) { | |
| String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); | |
| throw new OptionsParsingException(errorMsg); | |
| } | |
| } | |
| /** | |
| * Parses the args at the priority of the provided option. This is useful for after-the-fact | |
| * expansion. | |
| * | |
| * @param optionToExpand the option that is being "expanded" after the fact. The provided args | |
| * will have the same priority as this option. | |
| * @param source a description of where the expansion arguments came from. | |
| * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may | |
| * be in the following argument. | |
| */ | |
| public void parseArgsAsExpansionOfOption( | |
| ParsedOptionDescription optionToExpand, String source, List<String> args) | |
| throws OptionsParsingException { | |
| Preconditions.checkNotNull( | |
| optionToExpand, "Option for expansion not specified for arglist " + args); | |
| Preconditions.checkArgument( | |
| optionToExpand.getPriority().getPriorityCategory() | |
| != OptionPriority.PriorityCategory.DEFAULT, | |
| "Priority cannot be default, which was specified for arglist " + args); | |
| residue.addAll(impl.parseArgsAsExpansionOfOption(optionToExpand, o -> source, args)); | |
| if (!allowResidue && !residue.isEmpty()) { | |
| String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); | |
| throw new OptionsParsingException(errorMsg); | |
| } | |
| } | |
| /** | |
| * @param origin the origin of this option instance, it includes the priority of the value. If | |
| * other values have already been or will be parsed at a higher priority, they might override | |
| * the provided value. If this option already has a value at this priority, this value will | |
| * have precedence, but this should be avoided, as it breaks order tracking. | |
| * @param option the option to add the value for. | |
| * @param value the value to add at the given priority. | |
| */ | |
| void addOptionValueAtSpecificPriority( | |
| OptionInstanceOrigin origin, OptionDefinition option, String value) | |
| throws OptionsParsingException { | |
| impl.addOptionValueAtSpecificPriority(origin, option, value); | |
| } | |
| /** | |
| * Clears the given option. | |
| * | |
| * <p>This will not affect options objects that have already been retrieved from this parser | |
| * through {@link #getOptions(Class)}. | |
| * | |
| * @param option The option to clear. | |
| * @return The old value of the option that was cleared. | |
| * @throws IllegalArgumentException If the flag does not exist. | |
| */ | |
| public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException { | |
| return impl.clearValue(option); | |
| } | |
| @Override | |
| public List<String> getResidue() { | |
| return ImmutableList.copyOf(residue); | |
| } | |
| /** Returns a list of warnings about problems encountered by previous parse calls. */ | |
| public List<String> getWarnings() { | |
| return impl.getWarnings(); | |
| } | |
| @Override | |
| public <O extends OptionsBase> O getOptions(Class<O> optionsClass) { | |
| return impl.getParsedOptions(optionsClass); | |
| } | |
| @Override | |
| public boolean containsExplicitOption(String name) { | |
| return impl.containsExplicitOption(name); | |
| } | |
| @Override | |
| public List<ParsedOptionDescription> asCompleteListOfParsedOptions() { | |
| return impl.asCompleteListOfParsedOptions(); | |
| } | |
| @Override | |
| public List<ParsedOptionDescription> asListOfExplicitOptions() { | |
| return impl.asListOfExplicitOptions(); | |
| } | |
| @Override | |
| public List<ParsedOptionDescription> asListOfCanonicalOptions() { | |
| return impl.asCanonicalizedListOfParsedOptions(); | |
| } | |
| @Override | |
| public List<OptionValueDescription> asListOfOptionValues() { | |
| return impl.asListOfEffectiveOptions(); | |
| } | |
| @Override | |
| public List<String> canonicalize() { | |
| return impl.asCanonicalizedList(); | |
| } | |
| /** Returns all options fields of the given options class, in alphabetic order. */ | |
| public static ImmutableList<OptionDefinition> getOptionDefinitions( | |
| Class<? extends OptionsBase> optionsClass) { | |
| return OptionsData.getAllOptionDefinitionsForClass(optionsClass); | |
| } | |
| /** | |
| * Returns whether the given options class uses only the core types listed in {@link | |
| * UsesOnlyCoreTypes#CORE_TYPES}. These are guaranteed to be deeply immutable and serializable. | |
| */ | |
| public static boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) { | |
| OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); | |
| return data.getUsesOnlyCoreTypes(optionsClass); | |
| } | |
| /** | |
| * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited | |
| * ones) to its value in {@code options}. | |
| * | |
| * <p>To save space, the map directly stores {@code Fields} instead of the {@code | |
| * OptionDefinitions}. | |
| * | |
| * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa. The | |
| * map entries appear sorted alphabetically by option name. | |
| * | |
| * <p>If {@code options} is an instance of a subclass of {@link OptionsBase}, any options defined | |
| * by the subclass are not included in the map, only the options declared in the provided class | |
| * are included. | |
| * | |
| * @throws IllegalArgumentException if {@code options} is not an instance of {@link OptionsBase} | |
| */ | |
| public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) { | |
| // Alphabetized due to getAllOptionDefinitionsForClass()'s order. | |
| Map<Field, Object> map = new LinkedHashMap<>(); | |
| for (OptionDefinition optionDefinition : | |
| OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { | |
| try { | |
| // Get the object value of the optionDefinition and place in map. | |
| map.put(optionDefinition.getField(), optionDefinition.getField().get(options)); | |
| } catch (IllegalAccessException e) { | |
| // All options fields of options classes should be public. | |
| throw new IllegalStateException(e); | |
| } catch (IllegalArgumentException e) { | |
| // This would indicate an inconsistency in the cached OptionsData. | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| return map; | |
| } | |
| /** | |
| * Given a mapping as returned by {@link #toMap}, and the options class it that its entries | |
| * correspond to, this constructs the corresponding instance of the options class. | |
| * | |
| * @param map Field to Object, expecting an entry for each field in the optionsClass. This | |
| * directly refers to the Field, without wrapping it in an OptionDefinition, see {@link | |
| * #toMap}. | |
| * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code | |
| * optionsClass}, with values of the appropriate type | |
| */ | |
| public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) { | |
| // Instantiate the options class. | |
| OptionsData data = getOptionsDataInternal(optionsClass); | |
| O optionsInstance; | |
| try { | |
| Constructor<O> constructor = data.getConstructor(optionsClass); | |
| Preconditions.checkNotNull(constructor, "No options class constructor available"); | |
| optionsInstance = constructor.newInstance(); | |
| } catch (ReflectiveOperationException e) { | |
| throw new IllegalStateException("Error while instantiating options class", e); | |
| } | |
| List<OptionDefinition> optionDefinitions = | |
| OptionsData.getAllOptionDefinitionsForClass(optionsClass); | |
| // Ensure all fields are covered, no extraneous fields. | |
| validateFieldsSets(optionsClass, new LinkedHashSet<Field>(map.keySet())); | |
| // Populate the instance. | |
| for (OptionDefinition optionDefinition : optionDefinitions) { | |
| // Non-null as per above check. | |
| Object value = map.get(optionDefinition.getField()); | |
| try { | |
| optionDefinition.getField().set(optionsInstance, value); | |
| } catch (IllegalAccessException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| // May also throw IllegalArgumentException if map value is ill typed. | |
| } | |
| return optionsInstance; | |
| } | |
| /** | |
| * Raises a pretty {@link IllegalArgumentException} if the provided set of fields is a complete | |
| * set for the optionsClass. | |
| * | |
| * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link | |
| * Option} annotation. | |
| */ | |
| private static void validateFieldsSets( | |
| Class<? extends OptionsBase> optionsClass, LinkedHashSet<Field> fieldsFromMap) { | |
| ImmutableList<OptionDefinition> optionDefsFromClasses = | |
| OptionsData.getAllOptionDefinitionsForClass(optionsClass); | |
| Set<Field> fieldsFromClass = | |
| optionDefsFromClasses.stream().map(OptionDefinition::getField).collect(Collectors.toSet()); | |
| if (fieldsFromClass.equals(fieldsFromMap)) { | |
| // They are already equal, avoid additional checks. | |
| return; | |
| } | |
| List<String> extraNamesFromClass = new ArrayList<>(); | |
| List<String> extraNamesFromMap = new ArrayList<>(); | |
| for (OptionDefinition optionDefinition : optionDefsFromClasses) { | |
| if (!fieldsFromMap.contains(optionDefinition.getField())) { | |
| extraNamesFromClass.add("'" + optionDefinition.getOptionName() + "'"); | |
| } | |
| } | |
| for (Field field : fieldsFromMap) { | |
| // Extra validation on the map keys since they don't come from OptionsData. | |
| if (!fieldsFromClass.contains(field)) { | |
| if (field == null) { | |
| extraNamesFromMap.add("<null field>"); | |
| } else { | |
| OptionDefinition optionDefinition = null; | |
| try { | |
| // TODO(ccalvarin) This shouldn't be necessary, no option definitions should be found in | |
| // this optionsClass that weren't in the cache. | |
| optionDefinition = OptionDefinition.extractOptionDefinition(field); | |
| extraNamesFromMap.add("'" + optionDefinition.getOptionName() + "'"); | |
| } catch (NotAnOptionException e) { | |
| extraNamesFromMap.add("<non-Option field>"); | |
| } | |
| } | |
| } | |
| } | |
| throw new IllegalArgumentException( | |
| "Map keys do not match fields of options class; extra map keys: {" | |
| + Joiner.on(", ").join(extraNamesFromMap) | |
| + "}; extra options class options: {" | |
| + Joiner.on(", ").join(extraNamesFromClass) | |
| + "}"); | |
| } | |
| } |