522 lines
17 KiB
Java
522 lines
17 KiB
Java
|
// Copyright 2014 The Bazel Authors. All rights reserved.
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
package com.google.devtools.common.options;
|
||
|
|
||
|
import com.google.common.base.Splitter;
|
||
|
import com.google.common.collect.ImmutableList;
|
||
|
import com.google.common.collect.ImmutableMap;
|
||
|
import com.google.common.collect.Maps;
|
||
|
import java.time.Duration;
|
||
|
import java.util.Iterator;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.logging.Level;
|
||
|
import java.util.regex.Matcher;
|
||
|
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 {
|
||
|
|
||
|
/** Standard converter for booleans. Accepts common shorthands/synonyms. */
|
||
|
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";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for Strings. */
|
||
|
public static class StringConverter implements Converter<String> {
|
||
|
@Override
|
||
|
public String convert(String input) {
|
||
|
return input;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getTypeDescription() {
|
||
|
return "a string";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for integers. */
|
||
|
public static class IntegerConverter implements 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";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for longs. */
|
||
|
public static class LongConverter implements 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";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for doubles. */
|
||
|
public static class DoubleConverter implements 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";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for TriState values. */
|
||
|
public static class TriStateConverter implements 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)";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags
|
||
|
* are usually Void-typed and do not invoke the converter.
|
||
|
*/
|
||
|
public static class VoidConverter implements Converter<Void> {
|
||
|
@Override
|
||
|
public Void convert(String input) throws OptionsParsingException {
|
||
|
if (input == null || input.equals("null")) {
|
||
|
return null; // expected input, return is unused so null is fine.
|
||
|
}
|
||
|
throw new OptionsParsingException("'" + input + "' unexpected");
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getTypeDescription() {
|
||
|
return "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Standard converter for the {@link java.time.Duration} type. */
|
||
|
public static class DurationConverter implements Converter<Duration> {
|
||
|
private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$");
|
||
|
|
||
|
@Override
|
||
|
public Duration convert(String input) throws OptionsParsingException {
|
||
|
// To be compatible with the previous parser, '0' doesn't need a unit.
|
||
|
if ("0".equals(input)) {
|
||
|
return Duration.ZERO;
|
||
|
}
|
||
|
Matcher m = durationRegex.matcher(input);
|
||
|
if (!m.matches()) {
|
||
|
throw new OptionsParsingException("Illegal duration '" + input + "'.");
|
||
|
}
|
||
|
long duration = Long.parseLong(m.group(1));
|
||
|
String unit = m.group(2);
|
||
|
switch (unit) {
|
||
|
case "d":
|
||
|
return Duration.ofDays(duration);
|
||
|
case "h":
|
||
|
return Duration.ofHours(duration);
|
||
|
case "m":
|
||
|
return Duration.ofMinutes(duration);
|
||
|
case "s":
|
||
|
return Duration.ofSeconds(duration);
|
||
|
case "ms":
|
||
|
return Duration.ofMillis(duration);
|
||
|
default:
|
||
|
throw new IllegalStateException(
|
||
|
"This must not happen. Did you update the regex without the switch case?");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getTypeDescription() {
|
||
|
return "An immutable length of time.";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES.
|
||
|
/**
|
||
|
* The converters that are available to the options parser by default. These are used if the
|
||
|
* {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
|
||
|
* the following.
|
||
|
*/
|
||
|
public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS =
|
||
|
new ImmutableMap.Builder<Class<?>, Converter<?>>()
|
||
|
.put(String.class, new Converters.StringConverter())
|
||
|
.put(int.class, new Converters.IntegerConverter())
|
||
|
.put(long.class, new Converters.LongConverter())
|
||
|
.put(double.class, new Converters.DoubleConverter())
|
||
|
.put(boolean.class, new Converters.BooleanConverter())
|
||
|
.put(TriState.class, new Converters.TriStateConverter())
|
||
|
.put(Duration.class, new Converters.DurationConverter())
|
||
|
.put(Void.class, new Converters.VoidConverter())
|
||
|
.build();
|
||
|
|
||
|
/**
|
||
|
* 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.isEmpty() ? ImmutableList.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 final 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 | 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";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A converter for named integers of the form "[name=]value". When no name is specified, an empty
|
||
|
* string is used for the key.
|
||
|
*/
|
||
|
public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> {
|
||
|
|
||
|
@Override
|
||
|
public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException {
|
||
|
int pos = input.indexOf('=');
|
||
|
if (pos == 0 || input.length() == 0) {
|
||
|
throw new OptionsParsingException(
|
||
|
"Specify either 'value' or 'name=value', where 'value' is an integer");
|
||
|
} else if (pos < 0) {
|
||
|
try {
|
||
|
return Maps.immutableEntry("", Integer.parseInt(input));
|
||
|
} catch (NumberFormatException e) {
|
||
|
throw new OptionsParsingException("'" + input + "' is not an int");
|
||
|
}
|
||
|
}
|
||
|
String name = input.substring(0, pos);
|
||
|
String value = input.substring(pos + 1);
|
||
|
try {
|
||
|
return Maps.immutableEntry(name, Integer.parseInt(value));
|
||
|
} catch (NumberFormatException e) {
|
||
|
throw new OptionsParsingException("'" + value + "' is not an int");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getTypeDescription() {
|
||
|
return "an integer or a named integer, 'name=value'";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
|
||
|
public HelpVerbosityConverter() {
|
||
|
super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A converter to check whether an integer denoting a percentage is in a valid range: [0, 100].
|
||
|
*/
|
||
|
public static class PercentageConverter extends RangeConverter {
|
||
|
public PercentageConverter() {
|
||
|
super(0, 100);
|
||
|
}
|
||
|
}
|
||
|
}
|