| // 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; | |
| private final boolean allowEmptyValues; | |
| protected SeparatedOptionListConverter( | |
| char separator, String separatorDescription, boolean allowEmptyValues) { | |
| this.separatorDescription = separatorDescription; | |
| this.splitter = Splitter.on(separator); | |
| this.allowEmptyValues = allowEmptyValues; | |
| } | |
| @Override | |
| public List<String> convert(String input) throws OptionsParsingException { | |
| List<String> result = | |
| input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input)); | |
| if (!allowEmptyValues && result.contains("")) { | |
| // If the list contains exactly the empty string, it means an empty value was passed and we | |
| // should instead return an empty list. | |
| if (result.size() == 1) { | |
| return ImmutableList.of(); | |
| } | |
| throw new OptionsParsingException( | |
| "Empty values are not allowed as part of this " + getTypeDescription()); | |
| } | |
| return result; | |
| } | |
| @Override | |
| public String getTypeDescription() { | |
| return separatorDescription + "-separated list of options"; | |
| } | |
| } | |
| public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter { | |
| public CommaSeparatedOptionListConverter() { | |
| super(',', "comma", true); | |
| } | |
| } | |
| public static class CommaSeparatedNonEmptyOptionListConverter | |
| extends SeparatedOptionListConverter { | |
| public CommaSeparatedNonEmptyOptionListConverter() { | |
| super(',', "comma", false); | |
| } | |
| } | |
| public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter { | |
| public ColonSeparatedOptionListConverter() { | |
| super(':', "colon", true); | |
| } | |
| } | |
| 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<RegexPatternOption> { | |
| @Override | |
| public RegexPatternOption convert(String input) throws OptionsParsingException { | |
| try { | |
| return RegexPatternOption.create(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 {@code [name=]value1[,..,valueN]}, where names and | |
| * values are defined to be as permissive as possible. If no name is provided, "" is used. | |
| */ | |
| public static class AssignmentToListOfValuesConverter | |
| implements Converter<Map.Entry<String, List<String>>> { | |
| private final Splitter splitter = Splitter.on(','); | |
| @Override | |
| public Map.Entry<String, List<String>> convert(String input) throws OptionsParsingException { | |
| int pos = input.indexOf("="); | |
| String name = pos <= 0 ? "" : input.substring(0, pos); | |
| List<String> value = splitter.splitToList(input.substring(pos + 1)); | |
| if (value.contains("")) { | |
| // If the list contains exactly the empty string, it means an empty value was passed and we | |
| // should instead return an empty list. | |
| if (value.size() == 1) { | |
| value = ImmutableList.of(); | |
| } else { | |
| throw new OptionsParsingException( | |
| "Variable definitions must not contain empty strings or leading / trailing commas"); | |
| } | |
| } | |
| return Maps.immutableEntry(name, value); | |
| } | |
| @Override | |
| public String getTypeDescription() { | |
| return "a '[name=]value1[,..,valueN]' 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); | |
| } | |
| } | |
| } |