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.Maps;
     19 import java.util.Iterator;
     20 import java.util.List;
     21 import java.util.Map;
     22 import java.util.logging.Level;
     23 import java.util.regex.Pattern;
     24 import java.util.regex.PatternSyntaxException;
     25 
     26 /**
     27  * Some convenient converters used by blaze. Note: These are specific to
     28  * blaze.
     29  */
     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) {
    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   /**
    181    * The converters that are available to the options parser by default. These are used if the
    182    * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
    183    * the following.
    184    */
    185   static final Map<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap();
    186 
    187   static {
    188     DEFAULT_CONVERTERS.put(String.class, new Converters.StringConverter());
    189     DEFAULT_CONVERTERS.put(int.class, new Converters.IntegerConverter());
    190     DEFAULT_CONVERTERS.put(long.class, new Converters.LongConverter());
    191     DEFAULT_CONVERTERS.put(double.class, new Converters.DoubleConverter());
    192     DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
    193     DEFAULT_CONVERTERS.put(TriState.class, new Converters.TriStateConverter());
    194     DEFAULT_CONVERTERS.put(Void.class, new Converters.VoidConverter());
    195   }
    196 
    197   /**
    198    * Join a list of words as in English.  Examples:
    199    * "nothing"
    200    * "one"
    201    * "one or two"
    202    * "one and two"
    203    * "one, two or three".
    204    * "one, two and three".
    205    * The toString method of each element is used.
    206    */
    207   static String joinEnglishList(Iterable<?> choices) {
    208     StringBuilder buf = new StringBuilder();
    209     for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) {
    210       Object choice = ii.next();
    211       if (buf.length() > 0) {
    212         buf.append(ii.hasNext() ? ", " : " or ");
    213       }
    214       buf.append(choice);
    215     }
    216     return buf.length() == 0 ? "nothing" : buf.toString();
    217   }
    218 
    219   public static class SeparatedOptionListConverter
    220       implements Converter<List<String>> {
    221 
    222     private final String separatorDescription;
    223     private final Splitter splitter;
    224 
    225     protected SeparatedOptionListConverter(char separator,
    226                                            String separatorDescription) {
    227       this.separatorDescription = separatorDescription;
    228       this.splitter = Splitter.on(separator);
    229     }
    230 
    231     @Override
    232     public List<String> convert(String input) {
    233       return input.equals("")
    234           ? ImmutableList.<String>of()
    235           : ImmutableList.copyOf(splitter.split(input));
    236     }
    237 
    238     @Override
    239     public String getTypeDescription() {
    240       return separatorDescription + "-separated list of options";
    241     }
    242   }
    243 
    244   public static class CommaSeparatedOptionListConverter
    245       extends SeparatedOptionListConverter {
    246     public CommaSeparatedOptionListConverter() {
    247       super(',', "comma");
    248     }
    249   }
    250 
    251   public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter {
    252     public ColonSeparatedOptionListConverter() {
    253       super(':', "colon");
    254     }
    255   }
    256 
    257   public static class LogLevelConverter implements Converter<Level> {
    258 
    259     public static final Level[] LEVELS = new Level[] {
    260       Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE,
    261       Level.FINER, Level.FINEST
    262     };
    263 
    264     @Override
    265     public Level convert(String input) throws OptionsParsingException {
    266       try {
    267         int level = Integer.parseInt(input);
    268         return LEVELS[level];
    269       } catch (NumberFormatException e) {
    270         throw new OptionsParsingException("Not a log level: " + input);
    271       } catch (ArrayIndexOutOfBoundsException e) {
    272         throw new OptionsParsingException("Not a log level: " + input);
    273       }
    274     }
    275 
    276     @Override
    277     public String getTypeDescription() {
    278       return "0 <= an integer <= " + (LEVELS.length - 1);
    279     }
    280 
    281   }
    282 
    283   /**
    284    * Checks whether a string is part of a set of strings.
    285    */
    286   public static class StringSetConverter implements Converter<String> {
    287 
    288     // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/
    289     // here.
    290     private final List<String> values;
    291 
    292     public StringSetConverter(String... values) {
    293       this.values = ImmutableList.copyOf(values);
    294     }
    295 
    296     @Override
    297     public String convert(String input) throws OptionsParsingException {
    298       if (values.contains(input)) {
    299         return input;
    300       }
    301 
    302       throw new OptionsParsingException("Not one of " + values);
    303     }
    304 
    305     @Override
    306     public String getTypeDescription() {
    307       return joinEnglishList(values);
    308     }
    309   }
    310 
    311   /**
    312    * Checks whether a string is a valid regex pattern and compiles it.
    313    */
    314   public static class RegexPatternConverter implements Converter<Pattern> {
    315 
    316     @Override
    317     public Pattern convert(String input) throws OptionsParsingException {
    318       try {
    319         return Pattern.compile(input);
    320       } catch (PatternSyntaxException e) {
    321         throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage());
    322       }
    323     }
    324 
    325     @Override
    326     public String getTypeDescription() {
    327       return "a valid Java regular expression";
    328     }
    329   }
    330 
    331   /**
    332    * Limits the length of a string argument.
    333    */
    334   public static class LengthLimitingConverter implements Converter<String> {
    335     private final int maxSize;
    336 
    337     public LengthLimitingConverter(int maxSize) {
    338       this.maxSize = maxSize;
    339     }
    340 
    341     @Override
    342     public String convert(String input) throws OptionsParsingException {
    343       if (input.length() > maxSize) {
    344         throw new OptionsParsingException("Input must be " + getTypeDescription());
    345       }
    346       return input;
    347     }
    348 
    349     @Override
    350     public String getTypeDescription() {
    351       return "a string <= " + maxSize + " characters";
    352     }
    353   }
    354 
    355   /**
    356    * Checks whether an integer is in the given range.
    357    */
    358   public static class RangeConverter implements Converter<Integer> {
    359     final int minValue;
    360     final int maxValue;
    361 
    362     public RangeConverter(int minValue, int maxValue) {
    363       this.minValue = minValue;
    364       this.maxValue = maxValue;
    365     }
    366 
    367     @Override
    368     public Integer convert(String input) throws OptionsParsingException {
    369       try {
    370         Integer value = Integer.parseInt(input);
    371         if (value < minValue) {
    372           throw new OptionsParsingException("'" + input + "' should be >= " + minValue);
    373         } else if (value < minValue || value > maxValue) {
    374           throw new OptionsParsingException("'" + input + "' should be <= " + maxValue);
    375         }
    376         return value;
    377       } catch (NumberFormatException e) {
    378         throw new OptionsParsingException("'" + input + "' is not an int");
    379       }
    380     }
    381 
    382     @Override
    383     public String getTypeDescription() {
    384       if (minValue == Integer.MIN_VALUE) {
    385         if (maxValue == Integer.MAX_VALUE) {
    386           return "an integer";
    387         } else {
    388           return "an integer, <= " + maxValue;
    389         }
    390       } else if (maxValue == Integer.MAX_VALUE) {
    391         return "an integer, >= " + minValue;
    392       } else {
    393         return "an integer in "
    394             + (minValue < 0 ? "(" + minValue + ")" : minValue) + "-" + maxValue + " range";
    395       }
    396     }
    397   }
    398 
    399   /**
    400    * A converter for variable assignments from the parameter list of a blaze
    401    * command invocation. Assignments are expected to have the form "name=value",
    402    * where names and values are defined to be as permissive as possible.
    403    */
    404   public static class AssignmentConverter implements Converter<Map.Entry<String, String>> {
    405 
    406     @Override
    407     public Map.Entry<String, String> convert(String input)
    408         throws OptionsParsingException {
    409       int pos = input.indexOf("=");
    410       if (pos <= 0) {
    411         throw new OptionsParsingException("Variable definitions must be in the form of a "
    412             + "'name=value' assignment");
    413       }
    414       String name = input.substring(0, pos);
    415       String value = input.substring(pos + 1);
    416       return Maps.immutableEntry(name, value);
    417     }
    418 
    419     @Override
    420     public String getTypeDescription() {
    421       return "a 'name=value' assignment";
    422     }
    423 
    424   }
    425 
    426   /**
    427    * A converter for variable assignments from the parameter list of a blaze
    428    * command invocation. Assignments are expected to have the form "name[=value]",
    429    * where names and values are defined to be as permissive as possible and value
    430    * part can be optional (in which case it is considered to be null).
    431    */
    432   public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> {
    433 
    434     @Override
    435     public Map.Entry<String, String> convert(String input)
    436         throws OptionsParsingException {
    437       int pos = input.indexOf("=");
    438       if (pos == 0 || input.length() == 0) {
    439         throw new OptionsParsingException("Variable definitions must be in the form of a "
    440             + "'name=value' or 'name' assignment");
    441       } else if (pos < 0) {
    442         return Maps.immutableEntry(input, null);
    443       }
    444       String name = input.substring(0, pos);
    445       String value = input.substring(pos + 1);
    446       return Maps.immutableEntry(name, value);
    447     }
    448 
    449     @Override
    450     public String getTypeDescription() {
    451       return "a 'name=value' assignment with an optional value part";
    452     }
    453 
    454   }
    455 
    456   public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
    457     public HelpVerbosityConverter() {
    458       super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting");
    459     }
    460   }
    461 
    462 }
    463