com.google.devtools.common.options.OptionsParserImpl Maven / Gradle / Ivy
// 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.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The implementation of the options parser. This is intentionally package
* private for full flexibility. Use {@link OptionsParser} or {@link Options}
* if you're a consumer.
*/
class OptionsParserImpl {
/**
* A bunch of default converters in case the user doesn't specify a
* different one in the field annotation.
*/
static final Map, Converter>> DEFAULT_CONVERTERS = Maps.newHashMap();
static {
DEFAULT_CONVERTERS.put(String.class, new Converter() {
@Override
public String convert(String input) {
return input;
}
@Override
public String getTypeDescription() {
return "a string";
}});
DEFAULT_CONVERTERS.put(int.class, new Converter() {
@Override
public Integer convert(String input) throws OptionsParsingException {
try {
return Integer.decode(input);
} catch (NumberFormatException e) {
throw new OptionsParsingException("'" + input + "' is not an int");
}
}
@Override
public String getTypeDescription() {
return "an integer";
}});
DEFAULT_CONVERTERS.put(double.class, new Converter() {
@Override
public Double convert(String input) throws OptionsParsingException {
try {
return Double.parseDouble(input);
} catch (NumberFormatException e) {
throw new OptionsParsingException("'" + input + "' is not a double");
}
}
@Override
public String getTypeDescription() {
return "a double";
}});
DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
DEFAULT_CONVERTERS.put(TriState.class, new Converter() {
@Override
public TriState convert(String input) throws OptionsParsingException {
if (input == null) {
return TriState.AUTO;
}
input = input.toLowerCase();
if (input.equals("auto")) {
return TriState.AUTO;
}
if (input.equals("true") || input.equals("1") || input.equals("yes") ||
input.equals("t") || input.equals("y")) {
return TriState.YES;
}
if (input.equals("false") || input.equals("0") || input.equals("no") ||
input.equals("f") || input.equals("n")) {
return TriState.NO;
}
throw new OptionsParsingException("'" + input + "' is not a boolean");
}
@Override
public String getTypeDescription() {
return "a tri-state (auto, yes, no)";
}});
DEFAULT_CONVERTERS.put(Void.class, new Converter() {
@Override
public Void convert(String input) throws OptionsParsingException {
if (input == null) {
return null; // expected input, return is unused so null is fine.
}
throw new OptionsParsingException("'" + input + "' unexpected");
}
@Override
public String getTypeDescription() {
return "";
}});
DEFAULT_CONVERTERS.put(long.class, new Converter() {
@Override
public Long convert(String input) throws OptionsParsingException {
try {
return Long.decode(input);
} catch (NumberFormatException e) {
throw new OptionsParsingException("'" + input + "' is not a long");
}
}
@Override
public String getTypeDescription() {
return "a long integer";
}});
}
/**
* For every value, this class keeps track of its priority, its free-form source
* description, whether it was set as an implicit dependency, and the value.
*/
private static final class ParsedOptionEntry {
private final Object value;
private final OptionPriority priority;
private final String source;
private final String implicitDependant;
private final String expandedFrom;
private final boolean allowMultiple;
ParsedOptionEntry(Object value,
OptionPriority priority, String source, String implicitDependant, String expandedFrom,
boolean allowMultiple) {
this.value = value;
this.priority = priority;
this.source = source;
this.implicitDependant = implicitDependant;
this.expandedFrom = expandedFrom;
this.allowMultiple = allowMultiple;
}
// Need to suppress unchecked warnings, because the "multiple occurrence"
// options use unchecked ListMultimaps due to limitations of Java generics.
@SuppressWarnings({"unchecked", "rawtypes"})
Object getValue() {
if (allowMultiple) {
// Sort the results by option priority and return them in a new list.
// The generic type of the list is not known at runtime, so we can't
// use it here. It was already checked in the constructor, so this is
// type-safe.
List result = Lists.newArrayList();
ListMultimap realValue = (ListMultimap) value;
for (OptionPriority priority : OptionPriority.values()) {
// If there is no mapping for this key, this check avoids object creation (because
// ListMultimap has to return a new object on get) and also an unnecessary addAll call.
if (realValue.containsKey(priority)) {
result.addAll(realValue.get(priority));
}
}
return result;
}
return value;
}
// Need to suppress unchecked warnings, because the "multiple occurrence"
// options use unchecked ListMultimaps due to limitations of Java generics.
@SuppressWarnings({"unchecked", "rawtypes"})
void addValue(OptionPriority addedPriority, Object addedValue) {
Preconditions.checkState(allowMultiple);
ListMultimap optionValueList = (ListMultimap) value;
if (addedValue instanceof List>) {
for (Object element : (List>) addedValue) {
optionValueList.put(addedPriority, element);
}
} else {
optionValueList.put(addedPriority, addedValue);
}
}
OptionValueDescription asOptionValueDescription(String fieldName) {
return new OptionValueDescription(fieldName, getValue(), priority,
source, implicitDependant, expandedFrom);
}
}
private final OptionsData optionsData;
/**
* We store the results of parsing the arguments in here. It'll look like
*
* Field("--host") -> "www.google.com"
* Field("--port") -> 80
*
* This map is modified by repeated calls to
* {@link #parse(OptionPriority,Function,List)}.
*/
private final Map parsedValues = Maps.newHashMap();
/**
* We store the pre-parsed, explicit options for each priority in here.
* We use partially preparsed options, which can be different from the original
* representation, e.g. "--nofoo" becomes "--foo=0".
*/
private final List unparsedValues = Lists.newArrayList();
/**
* Unparsed values for use with the canonicalize command are stored separately from
* unparsedValues so that invocation policy can modify the values for canonicalization (e.g.
* override user-specified values with default values) without corrupting the data used to
* represent the user's original invocation for {@link #asListOfExplicitOptions()} and
* {@link #asListOfUnparsedOptions()}. A LinkedHashMultimap is used so that canonicalization
* happens in the correct order and multiple values can be stored for flags that allow multiple
* values.
*/
private final Multimap canonicalizeValues
= LinkedHashMultimap.create();
private final List warnings = Lists.newArrayList();
private boolean allowSingleDashLongOptions = false;
/**
* Create a new parser object
*/
OptionsParserImpl(OptionsData optionsData) {
this.optionsData = optionsData;
}
/**
* 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.
*/
void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
this.allowSingleDashLongOptions = allowSingleDashLongOptions;
}
/**
* The implementation of {@link OptionsBase#asMap}.
*/
static Map optionsAsMap(OptionsBase optionsInstance) {
Map map = Maps.newHashMap();
for (Field field : OptionsParser.getAllAnnotatedFields(optionsInstance.getClass())) {
try {
String name = field.getAnnotation(Option.class).name();
Object value = field.get(optionsInstance);
map.put(name, value);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e); // unreachable
}
}
return map;
}
List getAnnotatedFieldsFor(Class extends OptionsBase> clazz) {
return optionsData.getFieldsForClass(clazz);
}
/**
* Implements {@link OptionsParser#asListOfUnparsedOptions()}.
*/
List asListOfUnparsedOptions() {
List result = Lists.newArrayList(unparsedValues);
// It is vital that this sort is stable so that options on the same priority are not reordered.
Collections.sort(result, new Comparator() {
@Override
public int compare(UnparsedOptionValueDescription o1,
UnparsedOptionValueDescription o2) {
return o1.getPriority().compareTo(o2.getPriority());
}
});
return result;
}
/**
* Implements {@link OptionsParser#asListOfExplicitOptions()}.
*/
List asListOfExplicitOptions() {
List result = Lists.newArrayList(Iterables.filter(
unparsedValues,
new Predicate() {
@Override
public boolean apply(UnparsedOptionValueDescription input) {
return input.isExplicit();
}
}));
// It is vital that this sort is stable so that options on the same priority are not reordered.
Collections.sort(result, new Comparator() {
@Override
public int compare(UnparsedOptionValueDescription o1,
UnparsedOptionValueDescription o2) {
return o1.getPriority().compareTo(o2.getPriority());
}
});
return result;
}
/**
* Implements {@link OptionsParser#canonicalize}.
*/
List asCanonicalizedList() {
List processed = Lists.newArrayList(
canonicalizeValues.values());
// Sort implicit requirement options to the end, keeping their existing order, and sort the
// other options alphabetically.
Collections.sort(processed, new Comparator() {
@Override
public int compare(UnparsedOptionValueDescription o1, UnparsedOptionValueDescription o2) {
if (o1.isImplicitRequirement()) {
return o2.isImplicitRequirement() ? 0 : 1;
}
if (o2.isImplicitRequirement()) {
return -1;
}
return o1.getName().compareTo(o2.getName());
}
});
List result = Lists.newArrayList();
for (UnparsedOptionValueDescription value : processed) {
// Ignore expansion options.
if (value.isExpansion()) {
continue;
}
result.add("--" + value.getName() + "=" + value.getUnparsedValue());
}
return result;
}
/**
* Implements {@link OptionsParser#asListOfEffectiveOptions()}.
*/
List asListOfEffectiveOptions() {
List result = Lists.newArrayList();
for (Map.Entry mapEntry : optionsData.getAllNamedFields()) {
String fieldName = mapEntry.getKey();
Field field = mapEntry.getValue();
ParsedOptionEntry entry = parsedValues.get(field);
if (entry == null) {
Object value = optionsData.getDefaultValue(field);
result.add(new OptionValueDescription(fieldName, value, OptionPriority.DEFAULT,
null, null, null));
} else {
result.add(entry.asOptionValueDescription(fieldName));
}
}
return result;
}
Collection> getOptionsClasses() {
return optionsData.getOptionsClasses();
}
private void maybeAddDeprecationWarning(Field field) {
Option option = field.getAnnotation(Option.class);
// Continue to support the old behavior for @Deprecated options.
String warning = option.deprecationWarning();
if (!warning.isEmpty() || (field.getAnnotation(Deprecated.class) != null)) {
addDeprecationWarning(option.name(), warning);
}
}
private void addDeprecationWarning(String optionName, String warning) {
warnings.add("Option '" + optionName + "' is deprecated"
+ (warning.isEmpty() ? "" : ": " + warning));
}
// Warnings should not end with a '.' because the internal reporter adds one automatically.
private void setValue(Field field, String name, Object value,
OptionPriority priority, String source, String implicitDependant, String expandedFrom) {
ParsedOptionEntry entry = parsedValues.get(field);
if (entry != null) {
// Override existing option if the new value has higher or equal priority.
if (priority.compareTo(entry.priority) >= 0) {
// Output warnings:
if ((implicitDependant != null) && (entry.implicitDependant != null)) {
if (!implicitDependant.equals(entry.implicitDependant)) {
warnings.add("Option '" + name + "' is implicitly defined by both option '" +
entry.implicitDependant + "' and option '" + implicitDependant + "'");
}
} else if ((implicitDependant != null) && priority.equals(entry.priority)) {
warnings.add("Option '" + name + "' is implicitly defined by option '" +
implicitDependant + "'; the implicitly set value overrides the previous one");
} else if (entry.implicitDependant != null) {
warnings.add("A new value for option '" + name + "' overrides a previous " +
"implicit setting of that option by option '" + entry.implicitDependant + "'");
} else if ((priority == entry.priority) &&
((entry.expandedFrom == null) && (expandedFrom != null))) {
// Create a warning if an expansion option overrides an explicit option:
warnings.add("The option '" + expandedFrom + "' was expanded and now overrides a "
+ "previous explicitly specified option '" + name + "'");
}
// Record the new value:
parsedValues.put(field,
new ParsedOptionEntry(value, priority, source, implicitDependant, expandedFrom, false));
}
} else {
parsedValues.put(field,
new ParsedOptionEntry(value, priority, source, implicitDependant, expandedFrom, false));
maybeAddDeprecationWarning(field);
}
}
private void addListValue(Field field, Object value, OptionPriority priority, String source,
String implicitDependant, String expandedFrom) {
ParsedOptionEntry entry = parsedValues.get(field);
if (entry == null) {
entry = new ParsedOptionEntry(ArrayListMultimap.create(), priority, source,
implicitDependant, expandedFrom, true);
parsedValues.put(field, entry);
maybeAddDeprecationWarning(field);
}
entry.addValue(priority, value);
}
void clearValue(String optionName, Map clearedValues)
throws OptionsParsingException {
Field field = optionsData.getFieldFromName(optionName);
if (field == null) {
throw new IllegalArgumentException("No such option '" + optionName + "'");
}
Option option = field.getAnnotation(Option.class);
clearValue(field, option, clearedValues);
}
private void clearValue(
Field field, Option option, Map clearedValues)
throws OptionsParsingException {
ParsedOptionEntry removed = parsedValues.remove(field);
if (removed != null) {
clearedValues.put(option.name(), removed.asOptionValueDescription(option.name()));
}
canonicalizeValues.removeAll(field);
// Recurse to remove any implicit or expansion flags that this flag may have added when
// originally parsed.
for (String[] args : new String[][] {option.implicitRequirements(), option.expansion()}) {
Iterator argsIterator = Iterators.forArray(args);
while (argsIterator.hasNext()) {
String arg = argsIterator.next();
ParseOptionResult parseOptionResult = parseOption(arg, argsIterator);
clearValue(parseOptionResult.field, parseOptionResult.option, clearedValues);
}
}
}
OptionValueDescription getOptionValueDescription(String name) {
Field field = optionsData.getFieldFromName(name);
if (field == null) {
throw new IllegalArgumentException("No such option '" + name + "'");
}
ParsedOptionEntry entry = parsedValues.get(field);
if (entry == null) {
return null;
}
return entry.asOptionValueDescription(name);
}
OptionDescription getOptionDescription(String name) {
Field field = optionsData.getFieldFromName(name);
if (field == null) {
return null;
}
Option optionAnnotation = field.getAnnotation(Option.class);
return new OptionDescription(
name,
optionsData.getDefaultValue(field),
optionsData.getConverter(field),
optionAnnotation.allowMultiple());
}
boolean containsExplicitOption(String name) {
Field field = optionsData.getFieldFromName(name);
if (field == null) {
throw new IllegalArgumentException("No such option '" + name + "'");
}
return parsedValues.get(field) != null;
}
/**
* Parses the args, and returns what it doesn't parse. May be called multiple
* times, and may be called recursively. In each call, there may be no
* duplicates, but separate calls may contain intersecting sets of options; in
* that case, the arg seen last takes precedence.
*/
List parse(OptionPriority priority, Function super String, String> sourceFunction,
List args) throws OptionsParsingException {
return parse(priority, sourceFunction, null, null, args);
}
/**
* Parses the args, and returns what it doesn't parse. May be called multiple
* times, and may be called recursively. Calls may contain intersecting sets
* of options; in that case, the arg seen last takes precedence.
*
* The method uses the invariant that if an option has neither an implicit
* dependent nor an expanded from value, then it must have been explicitly
* set.
*/
private List parse(
OptionPriority priority,
Function super String, String> sourceFunction,
String implicitDependent,
String expandedFrom,
List args) throws OptionsParsingException {
List unparsedArgs = Lists.newArrayList();
LinkedHashMap> implicitRequirements = Maps.newLinkedHashMap();
Iterator argsIterator = args.iterator();
while (argsIterator.hasNext()) {
String arg = argsIterator.next();
if (!arg.startsWith("-")) {
unparsedArgs.add(arg);
continue; // not an option arg
}
if (arg.equals("--")) { // "--" means all remaining args aren't options
Iterators.addAll(unparsedArgs, argsIterator);
break;
}
ParseOptionResult optionParseResult = parseOption(arg, argsIterator);
Field field = optionParseResult.field;
Option option = optionParseResult.option;
String value = optionParseResult.value;
final String originalName = option.name();
if (option.wrapperOption()) {
if (value.startsWith("-")) {
List unparsed = parse(
priority,
Functions.constant("Unwrapped from wrapper option --" + originalName),
null, // implicitDependent
null, // expandedFrom
ImmutableList.of(value));
if (!unparsed.isEmpty()) {
throw new OptionsParsingException("Unparsed options remain after unwrapping " +
arg + ": " + Joiner.on(' ').join(unparsed));
}
// Don't process implicitRequirements or expansions for wrapper options. In particular,
// don't record this option in unparsedValues, so that only the wrapped option shows
// up in canonicalized options.
continue;
} else {
throw new OptionsParsingException("Invalid --" + originalName + " value format. "
+ "You may have meant --" + originalName + "=--" + value);
}
}
if (implicitDependent == null) {
// Log explicit options and expanded options in the order they are parsed (can be sorted
// later). Also remember whether they were expanded or not. This information is needed to
// correctly canonicalize flags.
UnparsedOptionValueDescription unparsedOptionValueDescription =
new UnparsedOptionValueDescription(
originalName,
field,
value,
priority,
sourceFunction.apply(originalName),
expandedFrom == null);
unparsedValues.add(unparsedOptionValueDescription);
if (option.allowMultiple()) {
canonicalizeValues.put(field, unparsedOptionValueDescription);
} else {
canonicalizeValues.replaceValues(field, ImmutableList.of(unparsedOptionValueDescription));
}
}
// Handle expansion options.
if (option.expansion().length > 0) {
Function