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