aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/common/options
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/common/options')
-rw-r--r--src/main/java/com/google/devtools/common/options/Converter.java33
-rw-r--r--src/main/java/com/google/devtools/common/options/Converters.java326
-rw-r--r--src/main/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java26
-rw-r--r--src/main/java/com/google/devtools/common/options/EnumConverter.java74
-rw-r--r--src/main/java/com/google/devtools/common/options/GenericTypeHelper.java133
-rw-r--r--src/main/java/com/google/devtools/common/options/Option.java127
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionPriority.java58
-rw-r--r--src/main/java/com/google/devtools/common/options/Options.java104
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsBase.java118
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsClassProvider.java29
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsData.java264
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsParser.java526
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsParserImpl.java722
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsParsingException.java50
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsProvider.java67
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsUsage.java156
-rw-r--r--src/main/java/com/google/devtools/common/options/TriState.java21
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&lt;String&gt; 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&lt;String&gt; 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
+}