diff options
Diffstat (limited to 'src/main/java/com/google/devtools/common/options')
17 files changed, 2834 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/common/options/Converter.java b/src/main/java/com/google/devtools/common/options/Converter.java new file mode 100644 index 0000000000..867ef8239d --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/Converter.java @@ -0,0 +1,33 @@ +// Copyright 2014 Google Inc. 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; + +/** + * A converter is a little helper object that can take a String and + * turn it into an instance of type T (the type parameter to the converter). + */ +public interface Converter<T> { + + /** + * Convert a string into type T. + */ + T convert(String input) throws OptionsParsingException; + + /** + * The type description appears in usage messages. E.g.: "a string", + * "a path", etc. + */ + String getTypeDescription(); + +} diff --git a/src/main/java/com/google/devtools/common/options/Converters.java b/src/main/java/com/google/devtools/common/options/Converters.java new file mode 100644 index 0000000000..e8c69ec070 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/Converters.java @@ -0,0 +1,326 @@ +// Copyright 2014 Google Inc. 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.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Some convenient converters used by blaze. Note: These are specific to + * blaze. + */ +public final class Converters { + + /** + * Join a list of words as in English. Examples: + * "nothing" + * "one" + * "one or two" + * "one and two" + * "one, two or three". + * "one, two and three". + * The toString method of each element is used. + */ + static String joinEnglishList(Iterable<?> choices) { + StringBuilder buf = new StringBuilder(); + for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) { + Object choice = ii.next(); + if (buf.length() > 0) { + buf.append(ii.hasNext() ? ", " : " or "); + } + buf.append(choice); + } + return buf.length() == 0 ? "nothing" : buf.toString(); + } + + public static class SeparatedOptionListConverter + implements Converter<List<String>> { + + private final String separatorDescription; + private final Splitter splitter; + + protected SeparatedOptionListConverter(char separator, + String separatorDescription) { + this.separatorDescription = separatorDescription; + this.splitter = Splitter.on(separator); + } + + @Override + public List<String> convert(String input) { + return input.equals("") + ? ImmutableList.<String>of() + : ImmutableList.copyOf(splitter.split(input)); + } + + @Override + public String getTypeDescription() { + return separatorDescription + "-separated list of options"; + } + } + + public static class CommaSeparatedOptionListConverter + extends SeparatedOptionListConverter { + public CommaSeparatedOptionListConverter() { + super(',', "comma"); + } + } + + public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter { + public ColonSeparatedOptionListConverter() { + super(':', "colon"); + } + } + + public static class LogLevelConverter implements Converter<Level> { + + public static Level[] LEVELS = new Level[] { + Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, + Level.FINER, Level.FINEST + }; + + @Override + public Level convert(String input) throws OptionsParsingException { + try { + int level = Integer.parseInt(input); + return LEVELS[level]; + } catch (NumberFormatException e) { + throw new OptionsParsingException("Not a log level: " + input); + } catch (ArrayIndexOutOfBoundsException e) { + throw new OptionsParsingException("Not a log level: " + input); + } + } + + @Override + public String getTypeDescription() { + return "0 <= an integer <= " + (LEVELS.length - 1); + } + + } + + /** + * Checks whether a string is part of a set of strings. + */ + public static class StringSetConverter implements Converter<String> { + + // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/ + // here. + private final List<String> values; + + public StringSetConverter(String... values) { + this.values = ImmutableList.copyOf(values); + } + + @Override + public String convert(String input) throws OptionsParsingException { + if (values.contains(input)) { + return input; + } + + throw new OptionsParsingException("Not one of " + values); + } + + @Override + public String getTypeDescription() { + return joinEnglishList(values); + } + } + + /** + * Checks whether a string is a valid regex pattern and compiles it. + */ + public static class RegexPatternConverter implements Converter<Pattern> { + + @Override + public Pattern convert(String input) throws OptionsParsingException { + try { + return Pattern.compile(input); + } catch (PatternSyntaxException e) { + throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage()); + } + } + + @Override + public String getTypeDescription() { + return "a valid Java regular expression"; + } + } + + /** + * Limits the length of a string argument. + */ + public static class LengthLimitingConverter implements Converter<String> { + private final int maxSize; + + public LengthLimitingConverter(int maxSize) { + this.maxSize = maxSize; + } + + @Override + public String convert(String input) throws OptionsParsingException { + if (input.length() > maxSize) { + throw new OptionsParsingException("Input must be " + getTypeDescription()); + } + return input; + } + + @Override + public String getTypeDescription() { + return "a string <= " + maxSize + " characters"; + } + } + + /** + * Checks whether an integer is in the given range. + */ + public static class RangeConverter implements Converter<Integer> { + final int minValue; + final int maxValue; + + public RangeConverter(int minValue, int maxValue) { + this.minValue = minValue; + this.maxValue = maxValue; + } + + @Override + public Integer convert(String input) throws OptionsParsingException { + try { + Integer value = Integer.parseInt(input); + if (value < minValue) { + throw new OptionsParsingException("'" + input + "' should be >= " + minValue); + } else if (value < minValue || value > maxValue) { + throw new OptionsParsingException("'" + input + "' should be <= " + maxValue); + } + return value; + } catch (NumberFormatException e) { + throw new OptionsParsingException("'" + input + "' is not an int"); + } + } + + @Override + public String getTypeDescription() { + if (minValue == Integer.MIN_VALUE) { + if (maxValue == Integer.MAX_VALUE) { + return "an integer"; + } else { + return "an integer, <= " + maxValue; + } + } else if (maxValue == Integer.MAX_VALUE) { + return "an integer, >= " + minValue; + } else { + return "an integer in " + + (minValue < 0 ? "(" + minValue + ")" : minValue) + "-" + maxValue + " range"; + } + } + } + + /** + * A converter for variable assignments from the parameter list of a blaze + * command invocation. Assignments are expected to have the form "name=value", + * where names and values are defined to be as permissive as possible. + */ + public static class AssignmentConverter implements Converter<Map.Entry<String, String>> { + + @Override + public Map.Entry<String, String> convert(String input) + throws OptionsParsingException { + int pos = input.indexOf("="); + if (pos <= 0) { + throw new OptionsParsingException("Variable definitions must be in the form of a " + + "'name=value' assignment"); + } + String name = input.substring(0, pos); + String value = input.substring(pos + 1); + return Maps.immutableEntry(name, value); + } + + @Override + public String getTypeDescription() { + return "a 'name=value' assignment"; + } + + } + + /** + * A converter for variable assignments from the parameter list of a blaze + * command invocation. Assignments are expected to have the form "name[=value]", + * where names and values are defined to be as permissive as possible and value + * part can be optional (in which case it is considered to be null). + */ + public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> { + + @Override + public Map.Entry<String, String> convert(String input) + throws OptionsParsingException { + int pos = input.indexOf("="); + if (pos == 0 || input.length() == 0) { + throw new OptionsParsingException("Variable definitions must be in the form of a " + + "'name=value' or 'name' assignment"); + } else if (pos < 0) { + return Maps.immutableEntry(input, null); + } + String name = input.substring(0, pos); + String value = input.substring(pos + 1); + return Maps.immutableEntry(name, value); + } + + @Override + public String getTypeDescription() { + return "a 'name=value' assignment with an optional value part"; + } + + } + + public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> { + public HelpVerbosityConverter() { + super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting"); + } + } + + /** + * A converter for boolean values. This is already one of the defaults, so clients + * should not typically need to add this. + */ + public static class BooleanConverter implements Converter<Boolean> { + @Override + public Boolean convert(String input) throws OptionsParsingException { + if (input == null) { + return false; + } + input = input.toLowerCase(); + if (input.equals("true") || input.equals("1") || input.equals("yes") || + input.equals("t") || input.equals("y")) { + return true; + } + if (input.equals("false") || input.equals("0") || input.equals("no") || + input.equals("f") || input.equals("n")) { + return false; + } + throw new OptionsParsingException("'" + input + "' is not a boolean"); + } + + @Override + public String getTypeDescription() { + return "a boolean"; + } + } + +} diff --git a/src/main/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java b/src/main/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java new file mode 100644 index 0000000000..b4e572ee1f --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java @@ -0,0 +1,26 @@ +// Copyright 2014 Google Inc. 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; + +/** + * Indicates that an option is declared in more than one class. + */ +public class DuplicateOptionDeclarationException extends RuntimeException { + + DuplicateOptionDeclarationException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/google/devtools/common/options/EnumConverter.java b/src/main/java/com/google/devtools/common/options/EnumConverter.java new file mode 100644 index 0000000000..f65241a214 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/EnumConverter.java @@ -0,0 +1,74 @@ +// Copyright 2014 Google Inc. 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 java.util.Arrays; + +/** + * A converter superclass for converters that parse enums. + * + * Just subclass this class, creating a zero aro argument constructor that + * calls {@link #EnumConverter(Class, String)}. + * + * This class compares the input string to the string returned by the toString() + * method of each enum member in a case-insensitive way. Usually, this is the + * name of the symbol, but beware if you override toString()! + */ +public abstract class EnumConverter<T extends Enum<T>> + implements Converter<T> { + + private final Class<T> enumType; + private final String typeName; + + /** + * Creates a new enum converter. You *must* implement a zero-argument + * constructor that delegates to this constructor, passing in the appropriate + * parameters. + * + * @param enumType The type of your enumeration; usually a class literal + * like MyEnum.class + * @param typeName The intuitive name of your enumeration, for example, the + * type name for CompilationMode might be "compilation mode". + */ + protected EnumConverter(Class<T> enumType, String typeName) { + this.enumType = enumType; + this.typeName = typeName; + } + + /** + * Implements {@link #convert(String)}. + */ + @Override + public final T convert(String input) throws OptionsParsingException { + for (T value : enumType.getEnumConstants()) { + if (value.toString().equalsIgnoreCase(input)) { + return value; + } + } + throw new OptionsParsingException("Not a valid " + typeName + ": '" + + input + "' (should be " + + getTypeDescription() + ")"); + } + + /** + * Implements {@link #getTypeDescription()}. + */ + @Override + public final String getTypeDescription() { + return Converters.joinEnglishList( + Arrays.asList(enumType.getEnumConstants())).toLowerCase(); + } + +} diff --git a/src/main/java/com/google/devtools/common/options/GenericTypeHelper.java b/src/main/java/com/google/devtools/common/options/GenericTypeHelper.java new file mode 100644 index 0000000000..2240860377 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/GenericTypeHelper.java @@ -0,0 +1,133 @@ +// Copyright 2014 Google Inc. 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.annotations.VisibleForTesting; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +/** + * A helper class for {@link OptionsParserImpl} to help checking the return type + * of a {@link Converter} against the type of a field or the element type of a + * list. + * + * <p>This class has to go through considerable contortion to get the correct result + * from the Java reflection system, unfortunately. If the generic reflection part + * had been better designed, some of this would not be necessary. + */ +class GenericTypeHelper { + + /** + * Returns the raw type of t, if t is either a raw or parameterized type. + * Otherwise, this method throws an {@link AssertionError}. + */ + @VisibleForTesting + static Class<?> getRawType(Type t) { + if (t instanceof Class<?>) { + return (Class<?>) t; + } else if (t instanceof ParameterizedType) { + return (Class<?>) ((ParameterizedType) t).getRawType(); + } else { + throw new AssertionError("A known concrete type is not concrete"); + } + } + + /** + * If type is a parameterized type, searches the given type variable in the list + * of declared type variables, and then returns the corresponding actual type. + * Returns null if the type variable is not defined by type. + */ + private static Type matchTypeVariable(Type type, TypeVariable<?> variable) { + if (type instanceof ParameterizedType) { + Class<?> rawInterfaceType = getRawType(type); + TypeVariable<?>[] typeParameters = rawInterfaceType.getTypeParameters(); + for (int i = 0; i < typeParameters.length; i++) { + if (variable.equals(typeParameters[i])) { + return ((ParameterizedType) type).getActualTypeArguments()[i]; + } + } + } + return null; + } + + /** + * Resolves the return type of a method, in particular if the generic return + * type ({@link Method#getGenericReturnType()}) is a type variable + * ({@link TypeVariable}), by checking all super-classes and directly + * implemented interfaces. + * + * <p>The method m must be defined by the given type or by its raw class type. + * + * @throws AssertionError if the generic return type could not be resolved + */ + // TODO(bazel-team): also check enclosing classes and indirectly implemented + // interfaces, which can also contribute type variables. This doesn't happen + // in the existing use cases. + public static Type getActualReturnType(Type type, Method method) { + Type returnType = method.getGenericReturnType(); + if (returnType instanceof Class<?>) { + return returnType; + } else if (returnType instanceof ParameterizedType) { + return returnType; + } else if (returnType instanceof TypeVariable<?>) { + TypeVariable<?> variable = (TypeVariable<?>) returnType; + while (type != null) { + Type candidate = matchTypeVariable(type, variable); + if (candidate != null) { + return candidate; + } + + Class<?> rawType = getRawType(type); + for (Type interfaceType : rawType.getGenericInterfaces()) { + candidate = matchTypeVariable(interfaceType, variable); + if (candidate != null) { + return candidate; + } + } + + type = rawType.getGenericSuperclass(); + } + } + throw new AssertionError("The type " + returnType + + " is not a Class, ParameterizedType, or TypeVariable"); + } + + /** + * Determines if a value of a particular type (from) is assignable to a field of + * a particular type (to). Also allows assigning wrapper types to primitive + * types. + * + * <p>The checks done here should be identical to the checks done by + * {@link java.lang.reflect.Field#set}. I.e., if this method returns true, a + * subsequent call to {@link java.lang.reflect.Field#set} should succeed. + */ + public static boolean isAssignableFrom(Type to, Type from) { + if (to instanceof Class<?>) { + Class<?> toClass = (Class<?>) to; + if (toClass.isPrimitive()) { + return Primitives.wrap(toClass).equals(from); + } + } + return TypeToken.of(to).isAssignableFrom(from); + } + + private GenericTypeHelper() { + // Prevents Java from creating a public constructor. + } +} diff --git a/src/main/java/com/google/devtools/common/options/Option.java b/src/main/java/com/google/devtools/common/options/Option.java new file mode 100644 index 0000000000..e2447362c2 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/Option.java @@ -0,0 +1,127 @@ +// Copyright 2014 Google Inc. 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An interface for annotating fields in classes (derived from OptionsBase) + * that are options. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Option { + + /** + * The name of the option ("--name"). + */ + String name(); + + /** + * The single-character abbreviation of the option ("-abbrev"). + */ + char abbrev() default '\0'; + + /** + * A help string for the usage information. + */ + String help() default ""; + + /** + * The default value for the option. This method should only be invoked + * directly by the parser implementation. Any access to default values + * should go via the parser to allow for application specific defaults. + * + * <p>There are two reasons this is a string. Firstly, it ensures that + * explicitly specifying this option at its default value (as printed in the + * usage message) has the same behavior as not specifying the option at all; + * this would be very hard to achieve if the default value was an instance of + * type T, since we'd need to ensure that {@link #toString()} and {@link + * #converter} were dual to each other. The second reason is more mundane + * but also more restrictive: annotation values must be compile-time + * constants. + * + * <p>If an option's defaultValue() is the string "null", the option's + * converter will not be invoked to interpret it; a null reference will be + * used instead. (It would be nice if defaultValue could simply return null, + * but bizarrely, the Java Language Specification does not consider null to + * be a compile-time constant.) This special interpretation of the string + * "null" is only applicable when computing the default value; if specified + * on the command-line, this string will have its usual literal meaning. + */ + String defaultValue(); + + /** + * A string describing the category of options that this belongs to. {@link + * OptionsParser#describeOptions} prints options of the same category grouped + * together. + */ + String category() default "misc"; + + /** + * The converter that we'll use to convert this option into an object or + * a simple type. The default is to use the builtin converters. + * Custom converters must implement the {@link Converter} interface. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + // Can't figure out how to coerce Converter.class into Class<? extends Converter<?>> + Class<? extends Converter> converter() default Converter.class; + + /** + * A flag indicating whether the option type should be allowed to occur + * multiple times in a single option list. + * + * <p>If the command can occur multiple times, then the attribute value + * <em>must</em> be a list type {@code List<T>}, and the result type of the + * converter for this option must either match the parameter {@code T} or + * {@code List<T>}. In the latter case the individual lists are concatenated + * to form the full options value. + */ + boolean allowMultiple() default false; + + /** + * If the option is actually an abbreviation for other options, this field will + * contain the strings to expand this option into. The original option is dropped + * and the replacement used in its stead. It is recommended that such an option be + * of type {@link Void}. + * + * An expanded option overrides previously specified options of the same name, + * even if it is explicitly specified. This is the original behavior and can + * be surprising if the user is not aware of it, which has led to several + * requests to change this behavior. This was discussed in the blaze team and + * it was decided that it is not a strong enough case to change the behavior. + */ + String[] expansion() default {}; + + /** + * If the option requires that additional options be implicitly appended, this field + * will contain the additional options. Implicit dependencies are parsed at the end + * of each {@link OptionsParser#parse} invocation, and override options specified in + * the same call. However, they can be overridden by options specified in a later + * call or by options with a higher priority. + * + * @see OptionPriority + */ + String[] implicitRequirements() default {}; + + /** + * If this field is a non-empty string, the option is deprecated, and a + * deprecation warning is added to the list of warnings when such an option + * is used. + */ + String deprecationWarning() default ""; +} diff --git a/src/main/java/com/google/devtools/common/options/OptionPriority.java b/src/main/java/com/google/devtools/common/options/OptionPriority.java new file mode 100644 index 0000000000..6e90008313 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionPriority.java @@ -0,0 +1,58 @@ +// Copyright 2014 Google Inc. 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; + +/** + * The priority of option values, in order of increasing priority. + * + * <p>In general, new values for options can only override values with a lower or + * equal priority. Option values provided in annotations in an options class are + * implicitly at the priority {@code DEFAULT}. + * + * <p>The ordering of the priorities is the source-code order. This is consistent + * with the automatically generated {@code compareTo} method as specified by the + * Java Language Specification. DO NOT change the source-code order of these + * values, or you will break code that relies on the ordering. + */ +public enum OptionPriority { + + /** + * The priority of values specified in the {@link Option} annotation. This + * should never be specified in calls to {@link OptionsParser#parse}. + */ + DEFAULT, + + /** + * Overrides default options at runtime, while still allowing the values to be + * overridden manually. + */ + COMPUTED_DEFAULT, + + /** + * For options coming from a configuration file or rc file. + */ + RC_FILE, + + /** + * For options coming from the command line. + */ + COMMAND_LINE, + + /** + * This priority can be used to unconditionally override any user-provided options. + * This should be used rarely and with caution! + */ + SOFTWARE_REQUIREMENT; + +} diff --git a/src/main/java/com/google/devtools/common/options/Options.java b/src/main/java/com/google/devtools/common/options/Options.java new file mode 100644 index 0000000000..171be2eb56 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/Options.java @@ -0,0 +1,104 @@ +// Copyright 2014 Google Inc. 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 java.util.Arrays; +import java.util.List; + +/** + * Interface for parsing options from a single options specification class. + * + * The {@link Options#parse(Class, String...)} method in this class has no clear + * use case. Instead, use the {@link OptionsParser} class directly, as in this + * code snippet: + * + * <pre> + * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class); + * try { + * parser.parse(FooOptions.class, args); + * } catch (OptionsParsingException e) { + * System.err.print("Error parsing options: " + e.getMessage()); + * System.err.print(options.getUsage()); + * System.exit(1); + * } + * FooOptions foo = parser.getOptions(FooOptions.class); + * List<String> otherArguments = parser.getResidue(); + * </pre> + * + * Using this class in this case actually results in more code. + * + * @see OptionsParser for parsing options from multiple options specification classes. + */ +public class Options<O extends OptionsBase> { + + /** + * Parse the options provided in args, given the specification in + * optionsClass. + */ + public static <O extends OptionsBase> Options<O> parse(Class<O> optionsClass, String... args) + throws OptionsParsingException { + OptionsParser parser = OptionsParser.newOptionsParser(optionsClass); + parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args)); + List<String> remainingArgs = parser.getResidue(); + return new Options<O>(parser.getOptions(optionsClass), + remainingArgs.toArray(new String[0])); + } + + /** + * Returns an options object at its default values. The returned object may + * be freely modified by the caller, by assigning its fields. + */ + public static <O extends OptionsBase> O getDefaults(Class<O> optionsClass) { + try { + return parse(optionsClass, new String[0]).getOptions(); + } catch (OptionsParsingException e) { + String message = "Error while parsing defaults: " + e.getMessage(); + throw new AssertionError(message); + } + } + + /** + * Returns a usage string (renders the help information, the defaults, and + * of course the option names). + */ + public static String getUsage(Class<? extends OptionsBase> optionsClass) { + StringBuilder usage = new StringBuilder(); + OptionsUsage.getUsage(optionsClass, usage); + return usage.toString(); + } + + private O options; + private String[] remainingArgs; + + private Options(O options, String[] remainingArgs) { + this.options = options; + this.remainingArgs = remainingArgs; + } + + /** + * Returns an instance of options class O. + */ + public O getOptions() { + return options; + } + + /** + * Returns the arguments that we didn't parse. + */ + public String[] getRemainingArgs() { + return remainingArgs; + } + +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsBase.java b/src/main/java/com/google/devtools/common/options/OptionsBase.java new file mode 100644 index 0000000000..ed9f2154f4 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsBase.java @@ -0,0 +1,118 @@ +// Copyright 2014 Google Inc. 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.escape.CharEscaperBuilder; +import com.google.common.escape.Escaper; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Base class for all options classes. Extend this class, adding public + * instance fields annotated with @Option. Then you can create instances + * either programmatically: + * + * <pre> + * X x = Options.getDefaults(X.class); + * x.host = "localhost"; + * x.port = 80; + * </pre> + * + * or from an array of command-line arguments: + * + * <pre> + * OptionsParser parser = OptionsParser.newOptionsParser(X.class); + * parser.parse("--host", "localhost", "--port", "80"); + * X x = parser.getOptions(X.class); + * </pre> + * + * <p>Subclasses of OptionsBase <b>must</b> be constructed reflectively, + * i.e. using not {@code new MyOptions}, but one of the two methods above + * instead. (Direct construction creates an empty instance, not containing + * default values. This leads to surprising behavior and often + * NullPointerExceptions, etc.) + */ +public abstract class OptionsBase { + + private static final Escaper ESCAPER = new CharEscaperBuilder() + .addEscape('\\', "\\\\").addEscape('"', "\\\"").toEscaper(); + + /** + * Subclasses must provide a default (no argument) constructor. + */ + protected OptionsBase() { + // There used to be a sanity check here that checks the stack trace of this constructor + // invocation; unfortunately, that makes the options construction about 10x slower. So be + // careful with how you construct options classes. + } + + /** + * Returns this options object in the form of a (new) mapping from option + * names, including inherited ones, to option values. If the public fields + * are mutated, this will be reflected in subsequent calls to {@code asMap}. + * Mutation of this map by the caller does not affect this options object. + */ + public final Map<String, Object> asMap() { + return OptionsParserImpl.optionsAsMap(this); + } + + @Override + public final String toString() { + return getClass().getName() + asMap(); + } + + /** + * Returns a string that uniquely identifies the options. This value is + * intended for analysis caching. + */ + public final String cacheKey() { + StringBuilder result = new StringBuilder(getClass().getName()).append("{"); + + for (Entry<String, Object> entry : asMap().entrySet()) { + result.append(entry.getKey()).append("="); + + Object value = entry.getValue(); + // This special case is needed because List.toString() prints the same + // ("[]") for an empty list and for a list with a single empty string. + if (value instanceof List<?> && ((List<?>) value).isEmpty()) { + result.append("EMPTY"); + } else if (value == null) { + result.append("NULL"); + } else { + result + .append('"') + .append(ESCAPER.escape(value.toString())) + .append('"'); + } + result.append(", "); + } + + return result.append("}").toString(); + } + + @Override + public final boolean equals(Object that) { + return that != null && + this.getClass() == that.getClass() && + this.asMap().equals(((OptionsBase) that).asMap()); + } + + @Override + public final int hashCode() { + return this.getClass().hashCode() + asMap().hashCode(); + } +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsClassProvider.java b/src/main/java/com/google/devtools/common/options/OptionsClassProvider.java new file mode 100644 index 0000000000..1868e23985 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsClassProvider.java @@ -0,0 +1,29 @@ +// Copyright 2014 Google Inc. 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; + +/** + * A read-only interface for options parser results, which only allows to query the options of + * a specific class, but not e.g. the residue any other information pertaining to the command line. + */ +public interface OptionsClassProvider { + /** + * Returns the options instance for the given {@code optionsClass}, that is, + * the parsed options, or null if it is not among those available. + * + * <p>The returned options should be treated by library code as immutable and + * a provider is permitted to return the same options instance multiple times. + */ + <O extends OptionsBase> O getOptions(Class<O> optionsClass); +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsData.java b/src/main/java/com/google/devtools/common/options/OptionsData.java new file mode 100644 index 0000000000..e9b6574b6e --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsData.java @@ -0,0 +1,264 @@ +// Copyright 2014 Google Inc. 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.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.concurrent.Immutable; + +/** + * An immutable selection of options data corresponding to a set of options + * classes. The data is collected using reflection, which can be expensive. + * Therefore this class can be used internally to cache the results. + */ +@Immutable +final class OptionsData { + + /** + * These are the options-declaring classes which are annotated with + * {@link Option} annotations. + */ + private final Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses; + + /** Maps option name to Option-annotated Field. */ + private final Map<String, Field> nameToField; + + /** Maps option abbreviation to Option-annotated Field. */ + private final Map<Character, Field> abbrevToField; + + /** + * For each options class, contains a list of all Option-annotated fields in + * that class. + */ + private final Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields; + + /** + * Mapping from each Option-annotated field to the default value for that + * field. + */ + private final Map<Field, Object> optionDefaults; + + /** + * Mapping from each Option-annotated field to the proper converter. + * + * @see OptionsParserImpl#findConverter + */ + private final Map<Field, Converter<?>> converters; + + private OptionsData(Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, + Map<String, Field> nameToField, + Map<Character, Field> abbrevToField, + Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields, + Map<Field, Object> optionDefaults, + Map<Field, Converter<?>> converters) { + this.optionsClasses = ImmutableMap.copyOf(optionsClasses); + this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields); + this.nameToField = ImmutableMap.copyOf(nameToField); + this.abbrevToField = ImmutableMap.copyOf(abbrevToField); + // Can't use an ImmutableMap here because of null values. + this.optionDefaults = Collections.unmodifiableMap(optionDefaults); + this.converters = ImmutableMap.copyOf(converters); + } + + public Collection<Class<? extends OptionsBase>> getOptionsClasses() { + return optionsClasses.keySet(); + } + + @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. + public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) { + return (Constructor<T>) optionsClasses.get(clazz); + } + + public Field getFieldFromName(String name) { + return nameToField.get(name); + } + + public Iterable<Map.Entry<String, Field>> getAllNamedFields() { + return nameToField.entrySet(); + } + + public Field getFieldForAbbrev(char abbrev) { + return abbrevToField.get(abbrev); + } + + public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) { + return allOptionsFields.get(optionsClass); + } + + public Object getDefaultValue(Field field) { + return optionDefaults.get(field); + } + + public Converter<?> getConverter(Field field) { + return converters.get(field); + } + + private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) { + List<Field> allFields = Lists.newArrayList(); + for (Field field : optionsClass.getFields()) { + if (field.isAnnotationPresent(Option.class)) { + allFields.add(field); + } + } + if (allFields.isEmpty()) { + throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields"); + } + return ImmutableList.copyOf(allFields); + } + + private static Object retrieveDefaultFromAnnotation(Field optionField) { + Option annotation = optionField.getAnnotation(Option.class); + // If an option can be specified multiple times, its default value is a new empty list. + if (annotation.allowMultiple()) { + return Collections.emptyList(); + } + String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); + try { + return OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField) + ? null + : OptionsParserImpl.findConverter(optionField).convert(defaultValueString); + } catch (OptionsParsingException e) { + throw new IllegalStateException("OptionsParsingException while " + + "retrieving default for " + optionField.getName() + ": " + + e.getMessage()); + } + } + + static OptionsData of(Collection<Class<? extends OptionsBase>> classes) { + Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap(); + Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap(); + Map<String, Field> nameToFieldBuilder = Maps.newHashMap(); + Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap(); + Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap(); + Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap(); + + // Read all Option annotations: + for (Class<? extends OptionsBase> parsedOptionsClass : classes) { + try { + Constructor<? extends OptionsBase> constructor = + parsedOptionsClass.getConstructor(new Class[0]); + constructorBuilder.put(parsedOptionsClass, constructor); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(parsedOptionsClass + + " lacks an accessible default constructor"); + } + List<Field> fields = getAllAnnotatedFields(parsedOptionsClass); + allOptionsFieldsBuilder.put(parsedOptionsClass, fields); + + for (Field field : fields) { + Option annotation = field.getAnnotation(Option.class); + + // Check that the field type is a List, and that the converter + // type matches the element type of the list. + Type fieldType = field.getGenericType(); + if (annotation.allowMultiple()) { + if (!(fieldType instanceof ParameterizedType)) { + throw new AssertionError("Type of multiple occurrence option must be a List<...>"); + } + ParameterizedType pfieldType = (ParameterizedType) fieldType; + if (pfieldType.getRawType() != List.class) { + // Throw an assertion, because this indicates an undetected type + // error in the code. + throw new AssertionError("Type of multiple occurrence option must be a List<...>"); + } + fieldType = pfieldType.getActualTypeArguments()[0]; + } + + // Get the converter return type. + @SuppressWarnings("rawtypes") + Class<? extends Converter> converter = annotation.converter(); + if (converter == Converter.class) { + Converter<?> actualConverter = OptionsParserImpl.DEFAULT_CONVERTERS.get(fieldType); + if (actualConverter == null) { + throw new AssertionError("Cannot find converter for field of type " + + field.getType() + " named " + field.getName() + + " in class " + field.getDeclaringClass().getName()); + } + converter = actualConverter.getClass(); + } + if (Modifier.isAbstract(converter.getModifiers())) { + throw new AssertionError("The converter type (" + converter + + ") must be a concrete type"); + } + Type converterResultType; + try { + Method convertMethod = converter.getMethod("convert", String.class); + converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod); + } catch (NoSuchMethodException e) { + throw new AssertionError("A known converter object doesn't implement the convert" + + " method"); + } + + if (annotation.allowMultiple()) { + if (GenericTypeHelper.getRawType(converterResultType) == List.class) { + Type elementType = + ((ParameterizedType) converterResultType).getActualTypeArguments()[0]; + if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) { + throw new AssertionError("If the converter return type of a multiple occurance " + + "option is a list, then the type of list elements (" + fieldType + ") must be " + + "assignable from the converter list element type (" + elementType + ")"); + } + } else { + if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { + throw new AssertionError("Type of list elements (" + fieldType + + ") for multiple occurrence option must be assignable from the converter " + + "return type (" + converterResultType + ")"); + } + } + } else { + if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { + throw new AssertionError("Type of field (" + fieldType + + ") must be assignable from the converter " + + "return type (" + converterResultType + ")"); + } + } + + if (annotation.name() == null) { + throw new AssertionError( + "Option cannot have a null name"); + } + if (nameToFieldBuilder.put(annotation.name(), field) != null) { + throw new DuplicateOptionDeclarationException( + "Duplicate option name: --" + annotation.name()); + } + if (annotation.abbrev() != '\0') { + if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) { + throw new DuplicateOptionDeclarationException( + "Duplicate option abbrev: -" + annotation.abbrev()); + } + } + optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field)); + + convertersBuilder.put(field, OptionsParserImpl.findConverter(field)); + } + } + return new OptionsData(constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder, + allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder); + } +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsParser.java b/src/main/java/com/google/devtools/common/options/OptionsParser.java new file mode 100644 index 0000000000..9564daa5a1 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java @@ -0,0 +1,526 @@ +// Copyright 2014 Google Inc. 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.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 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, String, String[])}, + * client code may call {@link #parse(OptionPriority,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 { + + /** + * 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 = + Maps.newHashMap(); + + private static synchronized OptionsData getOptionsData( + ImmutableList<Class<? extends OptionsBase>> optionsClasses) { + OptionsData result = optionsData.get(optionsClasses); + if (result == null) { + result = OptionsData.of(optionsClasses); + optionsData.put(optionsClasses, result); + } + return result; + } + + /** + * Returns all the annotated fields for the given class, including inherited + * ones. + */ + static Collection<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) { + OptionsData data = getOptionsData(ImmutableList.<Class<? extends OptionsBase>>of(optionsClass)); + return data.getFieldsForClass(optionsClass); + } + + /** + * @see #newOptionsParser(Iterable) + */ + public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1) { + return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1)); + } + + /** + * @see #newOptionsParser(Iterable) + */ + public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1, + Class<? extends OptionsBase> class2) { + return newOptionsParser(ImmutableList.of(class1, class2)); + } + + /** + * Create a new {@link OptionsParser}. + */ + public static OptionsParser newOptionsParser( + Iterable<Class<? extends OptionsBase>> optionsClasses) { + return new OptionsParser(getOptionsData(ImmutableList.copyOf(optionsClasses))); + } + + /** + * Canonicalizes a list of options using the given option classes. The + * contract is that if the returned set of options is passed to an options + * parser with the same options classes, then that will have the same effect + * as using the original args (which are passed in here), except for cosmetic + * differences. + */ + public static List<String> canonicalize( + Collection<Class<? extends OptionsBase>> optionsClasses, List<String> args) + throws OptionsParsingException { + OptionsParser parser = new OptionsParser(optionsClasses); + parser.setAllowResidue(false); + parser.parse(args); + return parser.impl.asCanonicalizedList(); + } + + private final OptionsParserImpl impl; + private final List<String> residue = new ArrayList<String>(); + private boolean allowResidue = true; + + OptionsParser(Collection<Class<? extends OptionsBase>> optionsClasses) { + this(OptionsData.of(optionsClasses)); + } + + 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); + } + + public void parseAndExitUponError(String[] args) { + parseAndExitUponError(OptionPriority.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 priority, String source, String[] args) { + 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); + } + for (String arg : args) { + if (arg.equals("--help")) { + System.out.println(describeOptions(Collections.<String, String>emptyMap(), + HelpVerbosity.LONG)); + System.exit(0); + } + } + } + + /** + * The name and value of an option with additional metadata describing its + * priority, source, whether it was set via an implicit dependency, and if so, + * by which other option. + */ + public static class OptionValueDescription { + private final String name; + private final Object value; + private final OptionPriority priority; + private final String source; + private final String implicitDependant; + private final String expandedFrom; + + public OptionValueDescription(String name, Object value, + OptionPriority priority, String source, String implicitDependant, String expandedFrom) { + this.name = name; + this.value = value; + this.priority = priority; + this.source = source; + this.implicitDependant = implicitDependant; + this.expandedFrom = expandedFrom; + } + + public String getName() { + return name; + } + + public Object getValue() { + return value; + } + + public OptionPriority getPriority() { + return priority; + } + + public String getSource() { + return source; + } + + public String getImplicitDependant() { + return implicitDependant; + } + + public boolean isImplicitDependency() { + return implicitDependant != null; + } + + public String getExpansionParent() { + return expandedFrom; + } + + public boolean isExpansion() { + return expandedFrom != null; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("option '").append(name).append("' "); + result.append("set to '").append(value).append("' "); + result.append("with priority ").append(priority); + if (source != null) { + result.append(" and source '").append(source).append("'"); + } + if (implicitDependant != null) { + result.append(" implicitly by "); + } + return result.toString(); + } + } + + /** + * The name and unparsed value of an option with additional metadata describing its + * priority, source, whether it was set via an implicit dependency, and if so, + * by which other option. + * + * <p>Note that the unparsed value and the source parameters can both be null. + */ + public static class UnparsedOptionValueDescription { + private final String name; + private final Field field; + private final String unparsedValue; + private final OptionPriority priority; + private final String source; + private final boolean explicit; + + public UnparsedOptionValueDescription(String name, Field field, String unparsedValue, + OptionPriority priority, String source, boolean explicit) { + this.name = name; + this.field = field; + this.unparsedValue = unparsedValue; + this.priority = priority; + this.source = source; + this.explicit = explicit; + } + + public String getName() { + return name; + } + + Field getField() { + return field; + } + + public boolean isBooleanOption() { + return field.getType().equals(boolean.class); + } + + private DocumentationLevel documentationLevel() { + Option option = field.getAnnotation(Option.class); + return OptionsParser.documentationLevel(option.category()); + } + + public boolean isDocumented() { + return documentationLevel() == DocumentationLevel.DOCUMENTED; + } + + public boolean isHidden() { + return documentationLevel() == DocumentationLevel.HIDDEN; + } + + boolean isExpansion() { + Option option = field.getAnnotation(Option.class); + return option.expansion().length > 0; + } + + boolean isImplicitRequirement() { + Option option = field.getAnnotation(Option.class); + return option.implicitRequirements().length > 0; + } + + boolean allowMultiple() { + Option option = field.getAnnotation(Option.class); + return option.allowMultiple(); + } + + public String getUnparsedValue() { + return unparsedValue; + } + + OptionPriority getPriority() { + return priority; + } + + public String getSource() { + return source; + } + + public boolean isExplicit() { + return explicit; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("option '").append(name).append("' "); + result.append("set to '").append(unparsedValue).append("' "); + result.append("with priority ").append(priority); + if (source != null) { + result.append(" and source '").append(source).append("'"); + } + return result.toString(); + } + } + + /** + * 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 } + + /** + * The level of documentation. Only documented options are output as part of + * the help. + * + * <p>We use 'hidden' so that options that form the protocol between the + * client and the server are not logged. + */ + enum DocumentationLevel { + DOCUMENTED, UNDOCUMENTED, HIDDEN + } + + /** + * 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. + * @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(Map<String, String> categoryDescriptions, + HelpVerbosity helpVerbosity) { + StringBuilder desc = new StringBuilder(); + if (!impl.getOptionsClasses().isEmpty()) { + + List<Field> allFields = Lists.newArrayList(); + for (Class<? extends OptionsBase> optionsClass : impl.getOptionsClasses()) { + allFields.addAll(impl.getAnnotatedFieldsFor(optionsClass)); + } + Collections.sort(allFields, OptionsUsage.BY_CATEGORY); + String prevCategory = null; + + for (Field optionField : allFields) { + String category = optionField.getAnnotation(Option.class).category(); + if (!category.equals(prevCategory)) { + prevCategory = category; + String description = categoryDescriptions.get(category); + if (description == null) { + description = "Options category '" + category + "'"; + } + if (documentationLevel(category) == DocumentationLevel.DOCUMENTED) { + desc.append("\n").append(description).append(":\n"); + } + } + + if (documentationLevel(prevCategory) == DocumentationLevel.DOCUMENTED) { + OptionsUsage.getUsage(optionField, desc, helpVerbosity); + } + } + } + return desc.toString().trim(); + } + + /** + * Returns a description of the option value set by the last previous call to + * {@link #parse(OptionPriority, 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. + */ + public OptionValueDescription getOptionValueDescription(String name) { + return impl.getOptionValueDescription(name); + } + + static DocumentationLevel documentationLevel(String category) { + if ("undocumented".equals(category)) { + return DocumentationLevel.UNDOCUMENTED; + } else if ("hidden".equals(category)) { + return DocumentationLevel.HIDDEN; + } else { + return DocumentationLevel.DOCUMENTED; + } + } + + /** + * A convenience method, equivalent to + * {@code parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args))}. + */ + public void parse(String... args) throws OptionsParsingException { + parse(OptionPriority.COMMAND_LINE, (String) null, Arrays.asList(args)); + } + + /** + * A convenience method, equivalent to + * {@code parse(OptionPriority.COMMAND_LINE, null, args)}. + */ + public void parse(List<String> args) throws OptionsParsingException { + parse(OptionPriority.COMMAND_LINE, (String) null, args); + } + + /** + * Parses {@code args}, using the classes registered with this parser. + * {@link #getOptions(Class)} and {@link #getResidue()} return the results. + * May be called multiple times; later options override existing ones if they + * have equal or higher priority. The source of options is a free-form string + * that can be used for debugging. Strings that cannot be parsed as options + * accumulates as residue, if this parser allows it. + * + * @see OptionPriority + */ + public void parse(OptionPriority priority, String source, + List<String> args) throws OptionsParsingException { + parseWithSourceFunction(priority, Functions.constant(source), args); + } + + /** + * Parses {@code args}, using the classes registered with this parser. + * {@link #getOptions(Class)} and {@link #getResidue()} return the results. May be called + * multiple times; later options override existing ones if they have equal or higher priority. + * The source of options is given as a function that maps option names to the source of the + * option. Strings that cannot be parsed as options accumulates as* residue, if this parser + * allows it. + */ + public void parseWithSourceFunction(OptionPriority priority, + Function<? super String, String> sourceFunction, List<String> args) + throws OptionsParsingException { + Preconditions.checkNotNull(priority); + Preconditions.checkArgument(priority != OptionPriority.DEFAULT); + residue.addAll(impl.parse(priority, sourceFunction, args)); + if (!allowResidue && !residue.isEmpty()) { + String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); + throw new OptionsParsingException(errorMsg); + } + } + + @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<UnparsedOptionValueDescription> asListOfUnparsedOptions() { + return impl.asListOfUnparsedOptions(); + } + + @Override + public List<UnparsedOptionValueDescription> asListOfExplicitOptions() { + return impl.asListOfExplicitOptions(); + } + + @Override + public List<OptionValueDescription> asListOfEffectiveOptions() { + return impl.asListOfEffectiveOptions(); + } +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java new file mode 100644 index 0000000000..e339dcd7f2 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -0,0 +1,722 @@ +// Copyright 2014 Google Inc. 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.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +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.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<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap(); + + static { + DEFAULT_CONVERTERS.put(String.class, new Converter<String>() { + @Override + public String convert(String input) { + return input; + } + @Override + public String getTypeDescription() { + return "a string"; + }}); + DEFAULT_CONVERTERS.put(int.class, new Converter<Integer>() { + @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<Double>() { + @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<TriState>() { + @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<Void>() { + @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<Long>() { + @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 + * <pre> + * Field("--host") -> "www.google.com" + * Field("--port") -> 80 + * </pre> + * This map is modified by repeated calls to + * {@link #parse(OptionPriority,Function,List)}. + */ + private final Map<Field, ParsedOptionEntry> 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<UnparsedOptionValueDescription> unparsedValues = + Lists.newArrayList(); + + private final List<String> 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<String, Object> optionsAsMap(OptionsBase optionsInstance) { + Map<String, Object> 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<Field> getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz) { + return optionsData.getFieldsForClass(clazz); + } + + /** + * Implements {@link OptionsParser#asListOfUnparsedOptions()}. + */ + List<UnparsedOptionValueDescription> asListOfUnparsedOptions() { + List<UnparsedOptionValueDescription> 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<UnparsedOptionValueDescription>() { + @Override + public int compare(UnparsedOptionValueDescription o1, + UnparsedOptionValueDescription o2) { + return o1.getPriority().compareTo(o2.getPriority()); + } + }); + return result; + } + + /** + * Implements {@link OptionsParser#asListOfExplicitOptions()}. + */ + List<UnparsedOptionValueDescription> asListOfExplicitOptions() { + List<UnparsedOptionValueDescription> result = Lists.newArrayList(Iterables.filter( + unparsedValues, + new Predicate<UnparsedOptionValueDescription>() { + @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<UnparsedOptionValueDescription>() { + @Override + public int compare(UnparsedOptionValueDescription o1, + UnparsedOptionValueDescription o2) { + return o1.getPriority().compareTo(o2.getPriority()); + } + }); + return result; + } + + /** + * Implements {@link OptionsParser#canonicalize}. + */ + List<String> asCanonicalizedList() { + List<UnparsedOptionValueDescription> processed = Lists.newArrayList(unparsedValues); + Collections.sort(processed, new Comparator<UnparsedOptionValueDescription>() { + // This Comparator sorts implicit requirement options to the end, keeping their existing + // order, and sorts the other options alphabetically. + @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<String> result = Lists.newArrayList(); + for (int i = 0; i < processed.size(); i++) { + UnparsedOptionValueDescription value = processed.get(i); + // Skip an option if the next option is the same, but only if the option does not allow + // multiple values. + if (!value.allowMultiple()) { + if ((i < processed.size() - 1) && value.getName().equals(processed.get(i + 1).getName())) { + continue; + } + } + + // Ignore expansion options. + if (value.isExpansion()) { + continue; + } + + result.add("--" + value.getName() + "=" + value.getUnparsedValue()); + } + return result; + } + + /** + * Implements {@link OptionsParser#asListOfEffectiveOptions()}. + */ + List<OptionValueDescription> asListOfEffectiveOptions() { + List<OptionValueDescription> result = Lists.newArrayList(); + for (Map.Entry<String,Field> 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<Class<? extends OptionsBase>> 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.equals("") || (field.getAnnotation(Deprecated.class) != null)) { + warnings.add("Option '" + option.name() + "' is deprecated" + + (warning.equals("") ? "" : ": " + 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, String name, 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); + } + + private Object getValue(Field field) { + ParsedOptionEntry entry = parsedValues.get(field); + return entry == null ? null : entry.getValue(); + } + + 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); + } + + 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<String> parse(OptionPriority priority, Function<? super String, String> sourceFunction, + List<String> 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. + * + * <p>The method uses the invariant that if an option has neither an implicit + * dependant nor an expanded from value, then it must have been explicitly + * set. + */ + private List<String> parse(OptionPriority priority, + final Function<? super String, String> sourceFunction, String implicitDependant, + String expandedFrom, List<String> args) throws OptionsParsingException { + List<String> unparsedArgs = Lists.newArrayList(); + LinkedHashMap<String,List<String>> implicitRequirements = Maps.newLinkedHashMap(); + for (int pos = 0; pos < args.size(); pos++) { + String arg = args.get(pos); + if (!arg.startsWith("-")) { + unparsedArgs.add(arg); + continue; // not an option arg + } + if (arg.equals("--")) { // "--" means all remaining args aren't options + while (++pos < args.size()) { + unparsedArgs.add(args.get(pos)); + } + break; + } + + String value = null; + Field field; + boolean booleanValue = true; + + if (arg.length() == 2) { // -l (may be nullary or unary) + field = optionsData.getFieldForAbbrev(arg.charAt(1)); + booleanValue = true; + + } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l- (boolean) + field = optionsData.getFieldForAbbrev(arg.charAt(1)); + booleanValue = false; + + } else if (allowSingleDashLongOptions // -long_option + || arg.startsWith("--")) { // or --long_option + int equalsAt = arg.indexOf('='); + int nameStartsAt = arg.startsWith("--") ? 2 : 1; + String name = + equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt); + if (name.trim().equals("")) { + throw new OptionsParsingException("Invalid options syntax: " + arg, arg); + } + value = equalsAt == -1 ? null : arg.substring(equalsAt + 1); + field = optionsData.getFieldFromName(name); + + // look for a "no"-prefixed option name: "no<optionname>"; + // (Undocumented: we also allow --no_foo. We're generous like that.) + if (field == null && name.startsWith("no")) { + String realname = name.substring(name.startsWith("no_") ? 3 : 2); + field = optionsData.getFieldFromName(realname); + booleanValue = false; + if (field != null) { + // TODO(bazel-team): Add tests for these cases. + if (!OptionsParserImpl.isBooleanField(field)) { + throw new OptionsParsingException( + "Illegal use of 'no' prefix on non-boolean option: " + arg, arg); + } + if (value != null) { + throw new OptionsParsingException( + "Unexpected value after boolean option: " + arg, arg); + } + // "no<optionname>" signifies a boolean option w/ false value + value = "0"; + } + } + + } else { + throw new OptionsParsingException("Invalid options syntax: " + arg, arg); + } + + if (field == null) { + throw new OptionsParsingException("Unrecognized option: " + arg, arg); + } + + if (value == null) { + // special case boolean to supply value based on presence of "no" prefix + if (OptionsParserImpl.isBooleanField(field)) { + value = booleanValue ? "1" : "0"; + } else if (field.getType().equals(Void.class)) { + // this is expected, Void type options have no args + } else if (pos != args.size() - 1) { + value = args.get(++pos); // "--flag value" form + } else { + throw new OptionsParsingException("Expected value after " + arg); + } + } + + Option option = field.getAnnotation(Option.class); + final String originalName = option.name(); + if (implicitDependant == 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. + unparsedValues.add(new UnparsedOptionValueDescription(originalName, field, value, + priority, sourceFunction.apply(originalName), expandedFrom == null)); + } + + // Handle expansion options. + if (option.expansion().length > 0) { + Function<Object, String> expansionSourceFunction = Functions.<String>constant( + "expanded from option --" + originalName + " from " + + sourceFunction.apply(originalName)); + maybeAddDeprecationWarning(field); + List<String> unparsed = parse(priority, expansionSourceFunction, null, originalName, + ImmutableList.copyOf(option.expansion())); + if (!unparsed.isEmpty()) { + // Throw an assertion, because this indicates an error in the code that specified the + // expansion for the current option. + throw new AssertionError("Unparsed options remain after parsing expansion of " + + arg + ":" + Joiner.on(' ').join(unparsed)); + } + } else { + Converter<?> converter = optionsData.getConverter(field); + Object convertedValue; + try { + convertedValue = converter.convert(value); + } catch (OptionsParsingException e) { + // The converter doesn't know the option name, so we supply it here by + // re-throwing: + throw new OptionsParsingException("While parsing option " + arg + + ": " + e.getMessage(), e); + } + + // ...but allow duplicates of single-use options across separate calls to + // parse(); latest wins: + if (!option.allowMultiple()) { + setValue(field, originalName, convertedValue, + priority, sourceFunction.apply(originalName), implicitDependant, expandedFrom); + } else { + // But if it's a multiple-use option, then just accumulate the + // values, in the order in which they were seen. + // Note: The type of the list member is not known; Java introspection + // only makes it available in String form via the signature string + // for the field declaration. + addListValue(field, originalName, convertedValue, + priority, sourceFunction.apply(originalName), implicitDependant, expandedFrom); + } + } + + // Collect any implicit requirements. + if (option.implicitRequirements().length > 0) { + implicitRequirements.put(option.name(), Arrays.asList(option.implicitRequirements())); + } + } + + // Now parse any implicit requirements that were collected. + // TODO(bazel-team): this should happen when the option is encountered. + if (!implicitRequirements.isEmpty()) { + for (Map.Entry<String,List<String>> entry : implicitRequirements.entrySet()) { + Function<Object, String> requirementSourceFunction = Functions.<String>constant( + "implicit requirement of option --" + entry.getKey() + " from " + + sourceFunction.apply(entry.getKey())); + + List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null, + entry.getValue()); + if (!unparsed.isEmpty()) { + // Throw an assertion, because this indicates an error in the code that specified in the + // implicit requirements for the option(s). + throw new AssertionError("Unparsed options remain after parsing implicit options:" + + Joiner.on(' ').join(unparsed)); + } + } + } + + return unparsedArgs; + } + + /** + * Gets the result of parsing the options. + */ + <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) { + // Create the instance: + O optionsInstance; + try { + Constructor<O> constructor = optionsData.getConstructor(optionsClass); + if (constructor == null) { + return null; + } + optionsInstance = constructor.newInstance(new Object[0]); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + // Set the fields + for (Field field : optionsData.getFieldsForClass(optionsClass)) { + Object value = getValue(field); + if (value == null) { + value = optionsData.getDefaultValue(field); + } + try { + field.set(optionsInstance, value); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + return optionsInstance; + } + + List<String> getWarnings() { + return ImmutableList.copyOf(warnings); + } + + static String getDefaultOptionString(Field optionField) { + Option annotation = optionField.getAnnotation(Option.class); + return annotation.defaultValue(); + } + + static boolean isBooleanField(Field field) { + return field.getType().equals(boolean.class) || field.getType().equals(TriState.class); + } + + static boolean isSpecialNullDefault(String defaultValueString, Field optionField) { + return defaultValueString.equals("null") && !optionField.getType().isPrimitive(); + } + + static Converter<?> findConverter(Field optionField) { + Option annotation = optionField.getAnnotation(Option.class); + if (annotation.converter() == Converter.class) { + Type type; + if (annotation.allowMultiple()) { + // The OptionParserImpl already checked that the type is List<T> for some T; + // here we extract the type T. + type = ((ParameterizedType) optionField.getGenericType()).getActualTypeArguments()[0]; + } else { + type = optionField.getType(); + } + Converter<?> converter = DEFAULT_CONVERTERS.get(type); + if (converter == null) { + throw new AssertionError("No converter found for " + + type + "; possible fix: add " + + "converter=... to @Option annotation for " + + optionField.getName()); + } + return converter; + } + try { + Class<?> converter = annotation.converter(); + Constructor<?> constructor = converter.getConstructor(new Class<?>[0]); + return (Converter<?>) constructor.newInstance(new Object[0]); + } catch (Exception e) { + throw new AssertionError(e); + } + } + +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsParsingException.java b/src/main/java/com/google/devtools/common/options/OptionsParsingException.java new file mode 100644 index 0000000000..9d2916ad87 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsParsingException.java @@ -0,0 +1,50 @@ +// Copyright 2014 Google Inc. 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; + +/** + * An exception that's thrown when the {@link OptionsParser} fails. + * + * @see OptionsParser#parse(OptionPriority,String,java.util.List) + */ +public class OptionsParsingException extends Exception { + private final String invalidArgument; + + public OptionsParsingException(String message) { + this(message, (String) null); + } + + public OptionsParsingException(String message, String argument) { + super(message); + this.invalidArgument = argument; + } + + public OptionsParsingException(String message, Throwable throwable) { + this(message, null, throwable); + } + + public OptionsParsingException(String message, String argument, Throwable throwable) { + super(message, throwable); + this.invalidArgument = argument; + } + + /** + * Gets the name of the invalid argument or {@code null} if the exception + * can not determine the exact invalid arguments + */ + public String getInvalidArgument() { + return invalidArgument; + } +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsProvider.java b/src/main/java/com/google/devtools/common/options/OptionsProvider.java new file mode 100644 index 0000000000..be399a73b9 --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsProvider.java @@ -0,0 +1,67 @@ +// Copyright 2014 Google Inc. 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.devtools.common.options.OptionsParser.OptionValueDescription; +import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription; + +import java.util.List; + +/** + * A read-only interface for options parser results, which does not allow any + * further parsing of options. + */ +public interface OptionsProvider extends OptionsClassProvider { + + /** + * Returns an immutable copy of the residue, that is, the arguments that + * have not been parsed. + */ + List<String> getResidue(); + + /** + * Returns if the named option was specified explicitly in a call to parse. + */ + boolean containsExplicitOption(String string); + + /** + * Returns a mutable copy of the list of all options that were specified + * either explicitly or implicitly. These options are sorted by priority, and + * by the order in which they were specified. If an option was specified + * multiple times, it is included in the result multiple times. Does not + * include the residue. + * + * <p>The returned list can be filtered if undocumented, hidden or implicit + * options should not be displayed. + */ + List<UnparsedOptionValueDescription> asListOfUnparsedOptions(); + + /** + * Returns a list of all explicitly specified options, suitable for logging + * or for displaying back to the user. These options are sorted by priority, + * and by the order in which they were specified. If an option was + * explicitly specified multiple times, it is included in the result + * multiple times. Does not include the residue. + * + * <p>The list includes undocumented options. + */ + public List<UnparsedOptionValueDescription> asListOfExplicitOptions(); + + /** + * Returns a list of all options, including undocumented ones, and their + * effective values. There is no guaranteed ordering for the result. + */ + public List<OptionValueDescription> asListOfEffectiveOptions(); +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsUsage.java b/src/main/java/com/google/devtools/common/options/OptionsUsage.java new file mode 100644 index 0000000000..c48a53295c --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionsUsage.java @@ -0,0 +1,156 @@ +// Copyright 2014 Google Inc. 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 static com.google.devtools.common.options.OptionsParserImpl.findConverter; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + +import java.lang.reflect.Field; +import java.text.BreakIterator; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A renderer for usage messages. For now this is very simple. + */ +class OptionsUsage { + + private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); + + /** + * Given an options class, render the usage string into the usage, + * which is passed in as an argument. + */ + static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) { + List<Field> optionFields = + Lists.newArrayList(OptionsParser.getAllAnnotatedFields(optionsClass)); + Collections.sort(optionFields, BY_NAME); + for (Field optionField : optionFields) { + getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG); + } + } + + /** + * Paragraph-fill the specified input text, indenting lines to 'indent' and + * wrapping lines at 'width'. Returns the formatted result. + */ + static String paragraphFill(String in, int indent, int width) { + String indentString = Strings.repeat(" ", indent); + StringBuilder out = new StringBuilder(); + String sep = ""; + for (String paragraph : NEWLINE_SPLITTER.split(in)) { + BreakIterator boundary = BreakIterator.getLineInstance(); // (factory) + boundary.setText(paragraph); + out.append(sep).append(indentString); + int cursor = indent; + for (int start = boundary.first(), end = boundary.next(); + end != BreakIterator.DONE; + start = end, end = boundary.next()) { + String word = + paragraph.substring(start, end); // (may include trailing space) + if (word.length() + cursor > width) { + out.append('\n').append(indentString); + cursor = indent; + } + out.append(word); + cursor += word.length(); + } + sep = "\n"; + } + return out.toString(); + } + + /** + * Append the usage message for a single option-field message to 'usage'. + */ + static void getUsage(Field optionField, StringBuilder usage, + OptionsParser.HelpVerbosity helpVerbosity) { + String flagName = getFlagName(optionField); + String typeDescription = getTypeDescription(optionField); + Option annotation = optionField.getAnnotation(Option.class); + usage.append(" --" + flagName); + if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name + usage.append('\n'); + return; + } + if (annotation.abbrev() != '\0') { + usage.append(" [-").append(annotation.abbrev()).append(']'); + } + if (!typeDescription.equals("")) { + usage.append(" (" + typeDescription + "; "); + if (annotation.allowMultiple()) { + usage.append("may be used multiple times"); + } else { + // Don't call the annotation directly (we must allow overrides to certain defaults) + String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); + if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { + usage.append("default: see description"); + } else { + usage.append("default: \"" + defaultValueString + "\""); + } + } + usage.append(")"); + } + usage.append("\n"); + if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type. + return; + } + if (!annotation.help().equals("")) { + usage.append(paragraphFill(annotation.help(), 4, 80)); // (indent, width) + usage.append('\n'); + } + if (annotation.expansion().length > 0) { + StringBuilder expandsMsg = new StringBuilder("Expands to: "); + for (String exp : annotation.expansion()) { + expandsMsg.append(exp).append(" "); + } + usage.append(paragraphFill(expandsMsg.toString(), 4, 80)); // (indent, width) + usage.append('\n'); + } + } + + private static final Comparator<Field> BY_NAME = new Comparator<Field>() { + @Override + public int compare(Field left, Field right) { + return left.getName().compareTo(right.getName()); + } + }; + + /** + * An ordering relation for option-field fields that first groups together + * options of the same category, then sorts by name within the category. + */ + static final Comparator<Field> BY_CATEGORY = new Comparator<Field>() { + @Override + public int compare(Field left, Field right) { + int r = left.getAnnotation(Option.class).category().compareTo( + right.getAnnotation(Option.class).category()); + return r == 0 ? BY_NAME.compare(left, right) : r; + } + }; + + private static String getTypeDescription(Field optionsField) { + return findConverter(optionsField).getTypeDescription(); + } + + static String getFlagName(Field field) { + String name = field.getAnnotation(Option.class).name(); + return OptionsParserImpl.isBooleanField(field) ? "[no]" + name : name; + } + +} diff --git a/src/main/java/com/google/devtools/common/options/TriState.java b/src/main/java/com/google/devtools/common/options/TriState.java new file mode 100644 index 0000000000..9e873eaf3a --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/TriState.java @@ -0,0 +1,21 @@ +// Copyright 2014 Google Inc. 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; + +/** + * Enum used to represent tri-state options (yes/no/auto). + */ +public enum TriState { + YES, NO, AUTO +} |