Home | History | Annotate | Download | only in options
      1 // Copyright 2014 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 package com.google.devtools.common.options;
     15 
     16 import com.google.common.base.Splitter;
     17 import com.google.common.collect.ImmutableList;
     18 import com.google.common.collect.ImmutableMap;
     19 import com.google.common.collect.Maps;
     20 import java.time.Duration;
     21 import java.util.Iterator;
     22 import java.util.List;
     23 import java.util.Map;
     24 import java.util.logging.Level;
     25 import java.util.regex.Matcher;
     26 import java.util.regex.Pattern;
     27 import java.util.regex.PatternSyntaxException;
     28 
     29 /** Some convenient converters used by blaze. Note: These are specific to blaze. */
     30 public final class Converters {
     31 
     32   /** Standard converter for booleans. Accepts common shorthands/synonyms. */
     33   public static class BooleanConverter implements Converter<Boolean> {
     34     @Override
     35     public Boolean convert(String input) throws OptionsParsingException {
     36       if (input == null) {
     37         return false;
     38       }
     39       input = input.toLowerCase();
     40       if (input.equals("true")
     41           || input.equals("1")
     42           || input.equals("yes")
     43           || input.equals("t")
     44           || input.equals("y")) {
     45         return true;
     46       }
     47       if (input.equals("false")
     48           || input.equals("0")
     49           || input.equals("no")
     50           || input.equals("f")
     51           || input.equals("n")) {
     52         return false;
     53       }
     54       throw new OptionsParsingException("'" + input + "' is not a boolean");
     55     }
     56 
     57     @Override
     58     public String getTypeDescription() {
     59       return "a boolean";
     60     }
     61   }
     62 
     63   /** Standard converter for Strings. */
     64   public static class StringConverter implements Converter<String> {
     65     @Override
     66     public String convert(String input) {
     67       return input;
     68     }
     69 
     70     @Override
     71     public String getTypeDescription() {
     72       return "a string";
     73     }
     74   }
     75 
     76   /** Standard converter for integers. */
     77   public static class IntegerConverter implements Converter<Integer> {
     78     @Override
     79     public Integer convert(String input) throws OptionsParsingException {
     80       try {
     81         return Integer.decode(input);
     82       } catch (NumberFormatException e) {
     83         throw new OptionsParsingException("'" + input + "' is not an int");
     84       }
     85     }
     86 
     87     @Override
     88     public String getTypeDescription() {
     89       return "an integer";
     90     }
     91   }
     92 
     93   /** Standard converter for longs. */
     94   public static class LongConverter implements Converter<Long> {
     95     @Override
     96     public Long convert(String input) throws OptionsParsingException {
     97       try {
     98         return Long.decode(input);
     99       } catch (NumberFormatException e) {
    100         throw new OptionsParsingException("'" + input + "' is not a long");
    101       }
    102     }
    103 
    104     @Override
    105     public String getTypeDescription() {
    106       return "a long integer";
    107     }
    108   }
    109 
    110   /** Standard converter for doubles. */
    111   public static class DoubleConverter implements Converter<Double> {
    112     @Override
    113     public Double convert(String input) throws OptionsParsingException {
    114       try {
    115         return Double.parseDouble(input);
    116       } catch (NumberFormatException e) {
    117         throw new OptionsParsingException("'" + input + "' is not a double");
    118       }
    119     }
    120 
    121     @Override
    122     public String getTypeDescription() {
    123       return "a double";
    124     }
    125   }
    126 
    127   /** Standard converter for TriState values. */
    128   public static class TriStateConverter implements Converter<TriState> {
    129     @Override
    130     public TriState convert(String input) throws OptionsParsingException {
    131       if (input == null) {
    132         return TriState.AUTO;
    133       }
    134       input = input.toLowerCase();
    135       if (input.equals("auto")) {
    136         return TriState.AUTO;
    137       }
    138       if (input.equals("true")
    139           || input.equals("1")
    140           || input.equals("yes")
    141           || input.equals("t")
    142           || input.equals("y")) {
    143         return TriState.YES;
    144       }
    145       if (input.equals("false")
    146           || input.equals("0")
    147           || input.equals("no")
    148           || input.equals("f")
    149           || input.equals("n")) {
    150         return TriState.NO;
    151       }
    152       throw new OptionsParsingException("'" + input + "' is not a boolean");
    153     }
    154 
    155     @Override
    156     public String getTypeDescription() {
    157       return "a tri-state (auto, yes, no)";
    158     }
    159   }
    160 
    161   /**
    162    * Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags
    163    * are usually Void-typed and do not invoke the converter.
    164    */
    165   public static class VoidConverter implements Converter<Void> {
    166     @Override
    167     public Void convert(String input) throws OptionsParsingException {
    168       if (input == null || input.equals("null")) {
    169         return null; // expected input, return is unused so null is fine.
    170       }
    171       throw new OptionsParsingException("'" + input + "' unexpected");
    172     }
    173 
    174     @Override
    175     public String getTypeDescription() {
    176       return "";
    177     }
    178   }
    179 
    180   /** Standard converter for the {@link java.time.Duration} type. */
    181   public static class DurationConverter implements Converter<Duration> {
    182     private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$");
    183 
    184     @Override
    185     public Duration convert(String input) throws OptionsParsingException {
    186       // To be compatible with the previous parser, '0' doesn't need a unit.
    187       if ("0".equals(input)) {
    188         return Duration.ZERO;
    189       }
    190       Matcher m = durationRegex.matcher(input);
    191       if (!m.matches()) {
    192         throw new OptionsParsingException("Illegal duration '" + input + "'.");
    193       }
    194       long duration = Long.parseLong(m.group(1));
    195       String unit = m.group(2);
    196       switch (unit) {
    197         case "d":
    198           return Duration.ofDays(duration);
    199         case "h":
    200           return Duration.ofHours(duration);
    201         case "m":
    202           return Duration.ofMinutes(duration);
    203         case "s":
    204           return Duration.ofSeconds(duration);
    205         case "ms":
    206           return Duration.ofMillis(duration);
    207         default:
    208           throw new IllegalStateException(
    209               "This must not happen. Did you update the regex without the switch case?");
    210       }
    211     }
    212 
    213     @Override
    214     public String getTypeDescription() {
    215       return "An immutable length of time.";
    216     }
    217   }
    218 
    219   // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES.
    220   /**
    221    * The converters that are available to the options parser by default. These are used if the
    222    * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
    223    * the following.
    224    */
    225   public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS =
    226       new ImmutableMap.Builder<Class<?>, Converter<?>>()
    227           .put(String.class, new Converters.StringConverter())
    228           .put(int.class, new Converters.IntegerConverter())
    229           .put(long.class, new Converters.LongConverter())
    230           .put(double.class, new Converters.DoubleConverter())
    231           .put(boolean.class, new Converters.BooleanConverter())
    232           .put(TriState.class, new Converters.TriStateConverter())
    233           .put(Duration.class, new Converters.DurationConverter())
    234           .put(Void.class, new Converters.VoidConverter())
    235           .build();
    236 
    237   /**
    238    * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one,
    239    * two or three". "one, two and three". The toString method of each element is used.
    240    */
    241   static String joinEnglishList(Iterable<?> choices) {
    242     StringBuilder buf = new StringBuilder();
    243     for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) {
    244       Object choice = ii.next();
    245       if (buf.length() > 0) {
    246         buf.append(ii.hasNext() ? ", " : " or ");
    247       }
    248       buf.append(choice);
    249     }
    250     return buf.length() == 0 ? "nothing" : buf.toString();
    251   }
    252 
    253   public static class SeparatedOptionListConverter implements Converter<List<String>> {
    254 
    255     private final String separatorDescription;
    256     private final Splitter splitter;
    257 
    258     protected SeparatedOptionListConverter(char separator, String separatorDescription) {
    259       this.separatorDescription = separatorDescription;
    260       this.splitter = Splitter.on(separator);
    261     }
    262 
    263     @Override
    264     public List<String> convert(String input) {
    265       return input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
    266     }
    267 
    268     @Override
    269     public String getTypeDescription() {
    270       return separatorDescription + "-separated list of options";
    271     }
    272   }
    273 
    274   public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
    275     public CommaSeparatedOptionListConverter() {
    276       super(',', "comma");
    277     }
    278   }
    279 
    280   public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter {
    281     public ColonSeparatedOptionListConverter() {
    282       super(':', "colon");
    283     }
    284   }
    285 
    286   public static class LogLevelConverter implements Converter<Level> {
    287 
    288     public static final Level[] LEVELS =
    289         new Level[] {
    290           Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, Level.FINER, Level.FINEST
    291         };
    292 
    293     @Override
    294     public Level convert(String input) throws OptionsParsingException {
    295       try {
    296         int level = Integer.parseInt(input);
    297         return LEVELS[level];
    298       } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
    299         throw new OptionsParsingException("Not a log level: " + input);
    300       }
    301     }
    302 
    303     @Override
    304     public String getTypeDescription() {
    305       return "0 <= an integer <= " + (LEVELS.length - 1);
    306     }
    307   }
    308 
    309   /** Checks whether a string is part of a set of strings. */
    310   public static class StringSetConverter implements Converter<String> {
    311 
    312     // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/
    313     // here.
    314     private final List<String> values;
    315 
    316     public StringSetConverter(String... values) {
    317       this.values = ImmutableList.copyOf(values);
    318     }
    319 
    320     @Override
    321     public String convert(String input) throws OptionsParsingException {
    322       if (values.contains(input)) {
    323         return input;
    324       }
    325 
    326       throw new OptionsParsingException("Not one of " + values);
    327     }
    328 
    329     @Override
    330     public String getTypeDescription() {
    331       return joinEnglishList(values);
    332     }
    333   }
    334 
    335   /** Checks whether a string is a valid regex pattern and compiles it. */
    336   public static class RegexPatternConverter implements Converter<Pattern> {
    337 
    338     @Override
    339     public Pattern convert(String input) throws OptionsParsingException {
    340       try {
    341         return Pattern.compile(input);
    342       } catch (PatternSyntaxException e) {
    343         throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage());
    344       }
    345     }
    346 
    347     @Override
    348     public String getTypeDescription() {
    349       return "a valid Java regular expression";
    350     }
    351   }
    352 
    353   /** Limits the length of a string argument. */
    354   public static class LengthLimitingConverter implements Converter<String> {
    355     private final int maxSize;
    356 
    357     public LengthLimitingConverter(int maxSize) {
    358       this.maxSize = maxSize;
    359     }
    360 
    361     @Override
    362     public String convert(String input) throws OptionsParsingException {
    363       if (input.length() > maxSize) {
    364         throw new OptionsParsingException("Input must be " + getTypeDescription());
    365       }
    366       return input;
    367     }
    368 
    369     @Override
    370     public String getTypeDescription() {
    371       return "a string <= " + maxSize + " characters";
    372     }
    373   }
    374 
    375   /** Checks whether an integer is in the given range. */
    376   public static class RangeConverter implements Converter<Integer> {
    377     final int minValue;
    378     final int maxValue;
    379 
    380     public RangeConverter(int minValue, int maxValue) {
    381       this.minValue = minValue;
    382       this.maxValue = maxValue;
    383     }
    384 
    385     @Override
    386     public Integer convert(String input) throws OptionsParsingException {
    387       try {
    388         Integer value = Integer.parseInt(input);
    389         if (value < minValue) {
    390           throw new OptionsParsingException("'" + input + "' should be >= " + minValue);
    391         } else if (value < minValue || value > maxValue) {
    392           throw new OptionsParsingException("'" + input + "' should be <= " + maxValue);
    393         }
    394         return value;
    395       } catch (NumberFormatException e) {
    396         throw new OptionsParsingException("'" + input + "' is not an int");
    397       }
    398     }
    399 
    400     @Override
    401     public String getTypeDescription() {
    402       if (minValue == Integer.MIN_VALUE) {
    403         if (maxValue == Integer.MAX_VALUE) {
    404           return "an integer";
    405         } else {
    406           return "an integer, <= " + maxValue;
    407         }
    408       } else if (maxValue == Integer.MAX_VALUE) {
    409         return "an integer, >= " + minValue;
    410       } else {
    411         return "an integer in "
    412             + (minValue < 0 ? "(" + minValue + ")" : minValue)
    413             + "-"
    414             + maxValue
    415             + " range";
    416       }
    417     }
    418   }
    419 
    420   /**
    421    * A converter for variable assignments from the parameter list of a blaze command invocation.
    422    * Assignments are expected to have the form "name=value", where names and values are defined to
    423    * be as permissive as possible.
    424    */
    425   public static class AssignmentConverter implements Converter<Map.Entry<String, String>> {
    426 
    427     @Override
    428     public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
    429       int pos = input.indexOf("=");
    430       if (pos <= 0) {
    431         throw new OptionsParsingException(
    432             "Variable definitions must be in the form of a 'name=value' assignment");
    433       }
    434       String name = input.substring(0, pos);
    435       String value = input.substring(pos + 1);
    436       return Maps.immutableEntry(name, value);
    437     }
    438 
    439     @Override
    440     public String getTypeDescription() {
    441       return "a 'name=value' assignment";
    442     }
    443   }
    444 
    445   /**
    446    * A converter for variable assignments from the parameter list of a blaze command invocation.
    447    * Assignments are expected to have the form "name[=value]", where names and values are defined to
    448    * be as permissive as possible and value part can be optional (in which case it is considered to
    449    * be null).
    450    */
    451   public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> {
    452 
    453     @Override
    454     public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
    455       int pos = input.indexOf('=');
    456       if (pos == 0 || input.length() == 0) {
    457         throw new OptionsParsingException(
    458             "Variable definitions must be in the form of a 'name=value' or 'name' assignment");
    459       } else if (pos < 0) {
    460         return Maps.immutableEntry(input, null);
    461       }
    462       String name = input.substring(0, pos);
    463       String value = input.substring(pos + 1);
    464       return Maps.immutableEntry(name, value);
    465     }
    466 
    467     @Override
    468     public String getTypeDescription() {
    469       return "a 'name=value' assignment with an optional value part";
    470     }
    471   }
    472 
    473   /**
    474    * A converter for named integers of the form "[name=]value". When no name is specified, an empty
    475    * string is used for the key.
    476    */
    477   public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> {
    478 
    479     @Override
    480     public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException {
    481       int pos = input.indexOf('=');
    482       if (pos == 0 || input.length() == 0) {
    483         throw new OptionsParsingException(
    484             "Specify either 'value' or 'name=value', where 'value' is an integer");
    485       } else if (pos < 0) {
    486         try {
    487           return Maps.immutableEntry("", Integer.parseInt(input));
    488         } catch (NumberFormatException e) {
    489           throw new OptionsParsingException("'" + input + "' is not an int");
    490         }
    491       }
    492       String name = input.substring(0, pos);
    493       String value = input.substring(pos + 1);
    494       try {
    495         return Maps.immutableEntry(name, Integer.parseInt(value));
    496       } catch (NumberFormatException e) {
    497         throw new OptionsParsingException("'" + value + "' is not an int");
    498       }
    499     }
    500 
    501     @Override
    502     public String getTypeDescription() {
    503       return "an integer or a named integer, 'name=value'";
    504     }
    505   }
    506 
    507   public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
    508     public HelpVerbosityConverter() {
    509       super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting");
    510     }
    511   }
    512 
    513   /**
    514    * A converter to check whether an integer denoting a percentage is in a valid range: [0, 100].
    515    */
    516   public static class PercentageConverter extends RangeConverter {
    517     public PercentageConverter() {
    518       super(0, 100);
    519     }
    520   }
    521 }
    522