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