1 package org.unicode.cldr.tool; 2 3 import com.google.common.base.Joiner; 4 import java.util.Arrays; 5 import java.util.Iterator; 6 import java.util.LinkedHashMap; 7 import java.util.LinkedHashSet; 8 import java.util.Map; 9 import java.util.Set; 10 import java.util.regex.Pattern; 11 import org.unicode.cldr.util.CLDRTool; 12 13 /** 14 * Simpler mechanism for handling options, where everything can be defined in one place. For an 15 * example, see {@link org.unicode.cldr.tool.DiffCldr.java} Note that before any enums are used, the 16 * main has to have MyOptions.parse(args, true); 17 * 18 * <ul> 19 * <li>The options and help message are defined in one place, for easier maintenance. 20 * <li>The options are represented by enums, for better type & syntax checking for problems. 21 * <li>The arguments can be checked against a regular expression. 22 * <li>The flag is defaulted to the first letter. 23 * <li>The options are printed at the top of the console output to document the exact input. 24 * <li>The callsite is slightly more verbose, but safer: 25 * <table> 26 * <tr><th>old</th><td>options[FILE_FILTER].value</td></tr> 27 * <tr><th>new</th><td>MyOptions.file_filter.option.getValue();</td></tr> 28 * </table> 29 * </ul> 30 * 31 * @author markdavis 32 */ 33 public class Option { 34 private final String tag; 35 private final Character flag; 36 private final Pattern match; 37 private final String defaultArgument; 38 private final String helpString; 39 // private final Enum<?> optionEnumValue; 40 private boolean doesOccur; 41 private String value; 42 43 /** 44 * Arguments for setting up options. Migration from UOption.create("generate_html", 'g', 45 * UOption.OPTIONAL_ARG).setDefault(CLDRPaths.CHART_DIRECTORY + "/errors/"), to: 46 * generate_html(new Params().setHelp" • UOption.NO_ARG: must have neither .setMatch nor 47 * .setDefault • UOption.REQUIRES_ARG: must have .setMatch but not setDefault • 48 * UOption.OPTIONAL_ARG: must have .setMatch and .setDefault (usually just copy over the 49 * .setDefault from the UOption) • Supply a meaningful .setHelp message • If the flag (the 'g' 50 * above) is different than the first letter of the enum, have a .setFlag 51 */ 52 public static class Params { 53 private Object match = null; 54 private String defaultArgument = null; 55 private String helpString = null; 56 private char flag = 0; 57 58 /** 59 * @param match the match to set 60 */ setMatch(Object match)61 public Params setMatch(Object match) { 62 this.match = match; 63 return this; 64 } 65 66 /** 67 * @param defaultArgument the defaultArgument to set 68 */ setDefault(String defaultArgument)69 public Params setDefault(String defaultArgument) { 70 this.defaultArgument = defaultArgument; 71 return this; 72 } 73 74 /** 75 * @param helpString the helpString to set 76 */ setHelp(String helpString)77 public Params setHelp(String helpString) { 78 this.helpString = helpString; 79 return this; 80 } 81 setFlag(char c)82 public Params setFlag(char c) { 83 flag = c; 84 return this; 85 } 86 } 87 88 // private boolean implicitValue; 89 clear()90 public void clear() { 91 doesOccur = false; 92 // implicitValue = false; 93 value = null; 94 } 95 getTag()96 public String getTag() { 97 return tag; 98 } 99 getMatch()100 public Pattern getMatch() { 101 return match; 102 } 103 getHelpString()104 public String getHelpString() { 105 return helpString; 106 } 107 getValue()108 public String getValue() { 109 return value; 110 } 111 getExplicitValue()112 public String getExplicitValue() { 113 return doesOccur ? value : null; 114 } 115 116 // public boolean getUsingImplicitValue() { 117 // return false; 118 // } 119 doesOccur()120 public boolean doesOccur() { 121 return doesOccur; 122 } 123 124 /** 125 * An option with no argument 126 * 127 * @see #doesOccur() 128 * @param optionEnumValue 129 * @param helpText 130 */ Option(Enum<?> optionEnumValue, String helpText)131 public Option(Enum<?> optionEnumValue, String helpText) { 132 this( 133 optionEnumValue, 134 optionEnumValue.name(), 135 (optionEnumValue.name().charAt(0)), 136 null, 137 null, 138 helpText); 139 } 140 Option( Enum<?> optionEnumValue, String argumentPattern, String defaultArgument, String helpText)141 public Option( 142 Enum<?> optionEnumValue, 143 String argumentPattern, 144 String defaultArgument, 145 String helpText) { 146 this( 147 optionEnumValue, 148 optionEnumValue.name(), 149 (optionEnumValue.name().charAt(0)), 150 Pattern.compile(argumentPattern), 151 defaultArgument, 152 helpText); 153 } 154 Option( Enum<?> enumOption, String tag, Character flag, Object argumentPatternIn, String defaultArgument, String helpString)155 public Option( 156 Enum<?> enumOption, 157 String tag, 158 Character flag, 159 Object argumentPatternIn, 160 String defaultArgument, 161 String helpString) { 162 Pattern argumentPattern = getPattern(argumentPatternIn); 163 164 if (defaultArgument != null && argumentPattern != null) { 165 if (!argumentPattern.matcher(defaultArgument).matches()) { 166 throw new IllegalArgumentException( 167 "Default argument doesn't match pattern: " 168 + defaultArgument 169 + ", " 170 + argumentPattern); 171 } 172 } 173 this.match = argumentPattern; 174 this.helpString = helpString; 175 this.tag = tag; 176 this.flag = flag; 177 this.defaultArgument = defaultArgument; 178 } 179 Option(Enum<?> optionEnumValue, Params optionList)180 public Option(Enum<?> optionEnumValue, Params optionList) { 181 this( 182 optionEnumValue, 183 optionEnumValue.name(), 184 optionList.flag != 0 ? optionList.flag : optionEnumValue.name().charAt(0), 185 optionList.match, 186 optionList.defaultArgument, 187 optionList.helpString); 188 } 189 getPattern(Object match)190 private static Pattern getPattern(Object match) { 191 if (match == null) { 192 return null; 193 } else if (match instanceof Pattern) { 194 return (Pattern) match; 195 } else if (match instanceof String) { 196 return Pattern.compile((String) match); 197 } else if (match instanceof Class) { 198 try { 199 Enum[] valuesMethod = (Enum[]) ((Class) match).getMethod("values").invoke(null); 200 return Pattern.compile(Joiner.on("|").join(valuesMethod)); 201 } catch (Exception e) { 202 throw new IllegalArgumentException(e); 203 } 204 } 205 throw new IllegalArgumentException(match.toString()); 206 } 207 208 static final String PAD = " "; 209 210 @Override toString()211 public String toString() { 212 return "-" 213 + flag 214 + " (" 215 + tag 216 + ")" 217 + PAD.substring(Math.min(tag.length(), PAD.length())) 218 + (match == null ? "no-arg" : "match: " + match.pattern()) 219 + (defaultArgument == null ? "" : " \tdefault=" + defaultArgument) 220 + " \t" 221 + helpString; 222 } 223 224 enum MatchResult { 225 noValueError, 226 noValue, 227 valueError, 228 value 229 } 230 matches(String inputValue)231 public MatchResult matches(String inputValue) { 232 if (doesOccur) { 233 System.err.println("#Duplicate argument: '" + tag); 234 return match == null ? MatchResult.noValueError : MatchResult.valueError; 235 } 236 doesOccur = true; 237 if (inputValue == null) { 238 inputValue = defaultArgument; 239 } 240 241 if (match == null) { 242 return MatchResult.noValue; 243 } else if (inputValue != null && match.matcher(inputValue).matches()) { 244 this.value = inputValue; 245 return MatchResult.value; 246 } else { 247 System.err.println( 248 "#The flag '" 249 + tag 250 + "' has the parameter '" 251 + inputValue 252 + "', which must match " 253 + match.pattern()); 254 return MatchResult.valueError; 255 } 256 } 257 258 public static class Options implements Iterable<Option> { 259 260 private String mainMessage; 261 final Map<String, Option> stringToValues = new LinkedHashMap<>(); 262 final Map<Enum<?>, Option> enumToValues = new LinkedHashMap<>(); 263 final Map<Character, Option> charToValues = new LinkedHashMap<>(); 264 final Set<String> results = new LinkedHashSet<>(); 265 266 { 267 add("help", null, "Provide the list of possible options"); 268 } 269 270 final Option help = charToValues.values().iterator().next(); 271 Options(String mainMessage)272 public Options(String mainMessage) { 273 this.mainMessage = 274 (mainMessage.isEmpty() ? "" : mainMessage + "\n") + "Here are the options:\n"; 275 } 276 Options()277 public Options() { 278 this(""); 279 } 280 281 /** 282 * Generate based on class and, optionally, CLDRTool annotation 283 * 284 * @param forClass 285 */ Options(Class<?> forClass)286 public Options(Class<?> forClass) { 287 this(forClass.getSimpleName() + ": " + getCLDRToolDescription(forClass)); 288 } 289 add(String string, String helpText)290 public Options add(String string, String helpText) { 291 return add(string, string.charAt(0), null, null, helpText); 292 } 293 add(String string, String argumentPattern, String helpText)294 public Options add(String string, String argumentPattern, String helpText) { 295 return add(string, string.charAt(0), argumentPattern, null, helpText); 296 } 297 add( String string, Object argumentPattern, String defaultArgument, String helpText)298 public Options add( 299 String string, Object argumentPattern, String defaultArgument, String helpText) { 300 return add(string, string.charAt(0), argumentPattern, defaultArgument, helpText); 301 } 302 add( Enum<?> optionEnumValue, Object argumentPattern, String defaultArgument, String helpText)303 public Option add( 304 Enum<?> optionEnumValue, 305 Object argumentPattern, 306 String defaultArgument, 307 String helpText) { 308 add( 309 optionEnumValue, 310 optionEnumValue.name(), 311 optionEnumValue.name().charAt(0), 312 argumentPattern, 313 defaultArgument, 314 helpText); 315 return get(optionEnumValue.name()); 316 // TODO cleanup 317 } 318 add( String string, Character flag, Object argumentPattern, String defaultArgument, String helpText)319 public Options add( 320 String string, 321 Character flag, 322 Object argumentPattern, 323 String defaultArgument, 324 String helpText) { 325 return add(null, string, flag, argumentPattern, defaultArgument, helpText); 326 } 327 add( Enum<?> optionEnumValue, String string, Character flag, Object argumentPattern, String defaultArgument, String helpText)328 public Options add( 329 Enum<?> optionEnumValue, 330 String string, 331 Character flag, 332 Object argumentPattern, 333 String defaultArgument, 334 String helpText) { 335 Option option = 336 new Option( 337 optionEnumValue, 338 string, 339 flag, 340 argumentPattern, 341 defaultArgument, 342 helpText); 343 return add(optionEnumValue, option); 344 } 345 add(Enum<?> optionEnumValue, Option option)346 public Options add(Enum<?> optionEnumValue, Option option) { 347 if (stringToValues.containsKey(option.tag)) { 348 throw new IllegalArgumentException( 349 "Duplicate tag <" 350 + option.tag 351 + "> with " 352 + stringToValues.get(option.tag)); 353 } 354 if (charToValues.containsKey(option.flag)) { 355 throw new IllegalArgumentException( 356 "Duplicate tag <" 357 + option.tag 358 + ", " 359 + option.flag 360 + "> with " 361 + charToValues.get(option.flag)); 362 } 363 stringToValues.put(option.tag, option); 364 charToValues.put(option.flag, option); 365 if (optionEnumValue != null) { 366 enumToValues.put(optionEnumValue, option); 367 } 368 return this; 369 } 370 parse(Enum<?> enumOption, String[] args, boolean showArguments)371 public Set<String> parse(Enum<?> enumOption, String[] args, boolean showArguments) { 372 return parse(args, showArguments); 373 } 374 parse(String[] args, boolean showArguments)375 public Set<String> parse(String[] args, boolean showArguments) { 376 results.clear(); 377 for (Option option : charToValues.values()) { 378 option.clear(); 379 } 380 int errorCount = 0; 381 boolean needHelp = false; 382 for (int i = 0; i < args.length; ++i) { 383 String arg = args[i]; 384 if (!arg.startsWith("-")) { 385 results.add(arg); 386 continue; 387 } 388 // can be of the form -fparam or -f param or --file param 389 boolean isStringOption = arg.startsWith("--"); 390 String value = null; 391 Option option; 392 if (isStringOption) { 393 arg = arg.substring(2); 394 int equalsPos = arg.indexOf('='); 395 if (equalsPos > -1) { 396 value = arg.substring(equalsPos + 1); 397 arg = arg.substring(0, equalsPos); 398 } 399 option = stringToValues.get(arg); 400 } else { // starts with single - 401 if (arg.length() > 2) { 402 value = arg.substring(2); 403 } 404 arg = arg.substring(1); 405 option = charToValues.get(arg.charAt(0)); 406 } 407 boolean tookExtraArgument = false; 408 if (value == null) { 409 value = i < args.length - 1 ? args[i + 1] : null; 410 if (value != null && value.startsWith("-")) { 411 value = null; 412 } 413 if (value != null) { 414 ++i; 415 tookExtraArgument = true; 416 } 417 } 418 if (option == null) { 419 ++errorCount; 420 System.out.println("#Unknown flag: " + arg); 421 } else { 422 MatchResult matches = option.matches(value); 423 if (tookExtraArgument 424 && (matches == MatchResult.noValue 425 || matches == MatchResult.noValueError)) { 426 --i; 427 } 428 if (option == help) { 429 needHelp = true; 430 } 431 } 432 } 433 // clean up defaults 434 for (Option option : stringToValues.values()) { 435 if (!option.doesOccur && option.defaultArgument != null) { 436 option.value = option.defaultArgument; 437 // option.implicitValue = true; 438 } 439 } 440 441 if (errorCount > 0) { 442 System.err.println("Invalid Option - Choices are:"); 443 System.err.println(getHelp()); 444 System.exit(1); 445 } else if (needHelp) { 446 System.err.println(getHelp()); 447 System.exit(1); 448 } else if (showArguments) { 449 System.out.println(Arrays.asList(args)); 450 for (Option option : stringToValues.values()) { 451 if (!option.doesOccur && option.value == null) { 452 continue; 453 } 454 System.out.println( 455 "#-" 456 + option.flag 457 + "\t" 458 + option.tag 459 + (option.doesOccur ? "\t≔\t" : "\t≝\t") 460 + option.value); 461 } 462 } 463 return results; 464 } 465 getHelp()466 public String getHelp() { 467 StringBuilder buffer = new StringBuilder(mainMessage); 468 boolean first = true; 469 for (Option option : stringToValues.values()) { 470 if (first) { 471 first = false; 472 } else { 473 buffer.append('\n'); 474 } 475 buffer.append(option); 476 } 477 return buffer.toString(); 478 } 479 480 @Override iterator()481 public Iterator<Option> iterator() { 482 return stringToValues.values().iterator(); 483 } 484 get(String string)485 public Option get(String string) { 486 Option result = stringToValues.get(string); 487 if (result == null) { 488 throw new IllegalArgumentException("Unknown option: " + string); 489 } 490 return result; 491 } 492 get(Enum<?> enumOption)493 public Option get(Enum<?> enumOption) { 494 Option result = enumToValues.get(enumOption); 495 if (result == null) { 496 throw new IllegalArgumentException("Unknown option: " + enumOption); 497 } 498 return result; 499 } 500 } 501 502 private enum Test { 503 A, 504 B, 505 C 506 } 507 508 static final Options myOptions = 509 new Options() 510 .add( 511 "file", 512 ".*", 513 "Filter the information based on file name, using a regex argument") 514 .add( 515 "path", 516 ".*", 517 "default-path", 518 "Filter the information based on path name, using a regex argument") 519 .add( 520 "content", 521 ".*", 522 "Filter the information based on content name, using a regex argument") 523 .add("gorp", null, null, "Gorp") 524 .add("enum", Test.class, null, "enum check") 525 .add("regex", "a*", null, "Gorp"); 526 main(String[] args)527 public static void main(String[] args) { 528 if (args.length == 0) { 529 args = "foo -fen.xml -c a* --path bar -g b -r aaa -e B".split("\\s+"); 530 } 531 myOptions.parse(args, true); 532 533 for (Option option : myOptions) { 534 System.out.println( 535 "#" 536 + option.getTag() 537 + "\t" 538 + option.doesOccur() 539 + "\t" 540 + option.getValue() 541 + "\t" 542 + option.getHelpString()); 543 } 544 Option option = myOptions.get("file"); 545 System.out.println("\n#" + option.doesOccur() + "\t" + option.getValue() + "\t" + option); 546 } 547 548 /** 549 * Helper function 550 * 551 * @param forClass 552 * @return 553 */ getCLDRToolDescription(Class<?> forClass)554 private static String getCLDRToolDescription(Class<?> forClass) { 555 CLDRTool cldrTool = forClass.getAnnotation(CLDRTool.class); 556 if (cldrTool != null) { 557 return cldrTool.description(); 558 } else { 559 return "(no @CLDRTool annotation)"; 560 } 561 } 562 getDefaultArgument()563 public String getDefaultArgument() { 564 return defaultArgument; 565 } 566 } 567