• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
convert(String input)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
getTypeDescription()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
convert(String input)66     public String convert(String input) {
67       return input;
68     }
69 
70     @Override
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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    */
joinEnglishList(Iterable<?> choices)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 
SeparatedOptionListConverter(char separator, String separatorDescription)258     protected SeparatedOptionListConverter(char separator, String separatorDescription) {
259       this.separatorDescription = separatorDescription;
260       this.splitter = Splitter.on(separator);
261     }
262 
263     @Override
convert(String input)264     public List<String> convert(String input) {
265       return input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
266     }
267 
268     @Override
getTypeDescription()269     public String getTypeDescription() {
270       return separatorDescription + "-separated list of options";
271     }
272   }
273 
274   public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
CommaSeparatedOptionListConverter()275     public CommaSeparatedOptionListConverter() {
276       super(',', "comma");
277     }
278   }
279 
280   public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter {
ColonSeparatedOptionListConverter()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
convert(String input)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
getTypeDescription()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 
StringSetConverter(String... values)316     public StringSetConverter(String... values) {
317       this.values = ImmutableList.copyOf(values);
318     }
319 
320     @Override
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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 
LengthLimitingConverter(int maxSize)357     public LengthLimitingConverter(int maxSize) {
358       this.maxSize = maxSize;
359     }
360 
361     @Override
convert(String input)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
getTypeDescription()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 
RangeConverter(int minValue, int maxValue)380     public RangeConverter(int minValue, int maxValue) {
381       this.minValue = minValue;
382       this.maxValue = maxValue;
383     }
384 
385     @Override
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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
convert(String input)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
getTypeDescription()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> {
HelpVerbosityConverter()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 {
PercentageConverter()517     public PercentageConverter() {
518       super(0, 100);
519     }
520   }
521 }
522