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