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