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 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) { 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 /** 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 { DEFAULT_CONVERTERS.put(String.class, new Converters.StringConverter())188 DEFAULT_CONVERTERS.put(String.class, new Converters.StringConverter()); DEFAULT_CONVERTERS.put(int.class, new Converters.IntegerConverter())189 DEFAULT_CONVERTERS.put(int.class, new Converters.IntegerConverter()); DEFAULT_CONVERTERS.put(long.class, new Converters.LongConverter())190 DEFAULT_CONVERTERS.put(long.class, new Converters.LongConverter()); DEFAULT_CONVERTERS.put(double.class, new Converters.DoubleConverter())191 DEFAULT_CONVERTERS.put(double.class, new Converters.DoubleConverter()); DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter())192 DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter()); DEFAULT_CONVERTERS.put(TriState.class, new Converters.TriStateConverter())193 DEFAULT_CONVERTERS.put(TriState.class, new Converters.TriStateConverter()); DEFAULT_CONVERTERS.put(Void.class, new Converters.VoidConverter())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 */ joinEnglishList(Iterable<?> choices)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 SeparatedOptionListConverter(char separator, String separatorDescription)225 protected SeparatedOptionListConverter(char separator, 226 String separatorDescription) { 227 this.separatorDescription = separatorDescription; 228 this.splitter = Splitter.on(separator); 229 } 230 231 @Override convert(String input)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 getTypeDescription()239 public String getTypeDescription() { 240 return separatorDescription + "-separated list of options"; 241 } 242 } 243 244 public static class CommaSeparatedOptionListConverter 245 extends SeparatedOptionListConverter { CommaSeparatedOptionListConverter()246 public CommaSeparatedOptionListConverter() { 247 super(',', "comma"); 248 } 249 } 250 251 public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter { ColonSeparatedOptionListConverter()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 convert(String input)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 getTypeDescription()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 StringSetConverter(String... values)292 public StringSetConverter(String... values) { 293 this.values = ImmutableList.copyOf(values); 294 } 295 296 @Override convert(String input)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 getTypeDescription()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 convert(String input)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 getTypeDescription()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 LengthLimitingConverter(int maxSize)337 public LengthLimitingConverter(int maxSize) { 338 this.maxSize = maxSize; 339 } 340 341 @Override convert(String input)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 getTypeDescription()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 RangeConverter(int minValue, int maxValue)362 public RangeConverter(int minValue, int maxValue) { 363 this.minValue = minValue; 364 this.maxValue = maxValue; 365 } 366 367 @Override convert(String input)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 getTypeDescription()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 convert(String input)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 getTypeDescription()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 convert(String input)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 getTypeDescription()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> { HelpVerbosityConverter()457 public HelpVerbosityConverter() { 458 super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting"); 459 } 460 } 461 462 } 463