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.ibm.icu.dev.util.CollectionUtilities; 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 */ 62 public Params setMatch(Object match) { 63 this.match = match; 64 return this; 65 } 66 67 /** 68 * @param defaultArgument the defaultArgument to set 69 */ 70 public Params setDefault(String defaultArgument) { 71 this.defaultArgument = defaultArgument; 72 return this; 73 } 74 75 /** 76 * @param helpString the helpString to set 77 */ 78 public Params setHelp(String helpString) { 79 this.helpString = helpString; 80 return this; 81 } 82 83 public Params setFlag(char c) { 84 flag = c; 85 return this; 86 } 87 } 88 89 // private boolean implicitValue; 90 91 public void clear() { 92 doesOccur = false; 93 // implicitValue = false; 94 value = null; 95 } 96 97 public String getTag() { 98 return tag; 99 } 100 101 public Pattern getMatch() { 102 return match; 103 } 104 105 public String getHelpString() { 106 return helpString; 107 } 108 109 public String getValue() { 110 return value; 111 } 112 113 public String getExplicitValue() { 114 return doesOccur ? value : null; 115 } 116 117 // public boolean getUsingImplicitValue() { 118 // return false; 119 // } 120 121 public boolean doesOccur() { 122 return doesOccur; 123 } 124 125 public Option(Enum<?> optionEnumValue, String argumentPattern, String defaultArgument, String helpText) { 126 this(optionEnumValue, optionEnumValue.name(), (Character) (optionEnumValue.name().charAt(0)), Pattern.compile(argumentPattern), defaultArgument, 127 helpText); 128 } 129 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 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 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(CollectionUtilities.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 public String toString() { 176 return "-" + flag 177 + " (" + tag + ")" 178 + PAD.substring(Math.min(tag.length(), PAD.length())) 179 + (match == null ? "no-arg" : "match: " + match.pattern()) 180 + (defaultArgument == null ? "" : " \tdefault=" + defaultArgument) 181 + " \t" + helpString; 182 } 183 184 enum MatchResult { 185 noValueError, noValue, valueError, value 186 } 187 188 public MatchResult matches(String inputValue) { 189 if (doesOccur) { 190 System.err.println("#Duplicate argument: '" + tag); 191 return match == null ? MatchResult.noValueError : MatchResult.valueError; 192 } 193 doesOccur = true; 194 if (inputValue == null) { 195 inputValue = defaultArgument; 196 } 197 198 if (match == null) { 199 return MatchResult.noValue; 200 } else if (inputValue != null && match.matcher(inputValue).matches()) { 201 this.value = inputValue; 202 return MatchResult.value; 203 } else { 204 System.err.println("#The flag '" + tag + "' has the parameter '" + inputValue + "', which must match " 205 + match.pattern()); 206 return MatchResult.valueError; 207 } 208 } 209 210 public static class Options implements Iterable<Option> { 211 212 private String mainMessage; 213 final Map<String, Option> stringToValues = new LinkedHashMap<String, Option>(); 214 final Map<Enum<?>, Option> enumToValues = new LinkedHashMap<Enum<?>, Option>(); 215 final Map<Character, Option> charToValues = new LinkedHashMap<Character, Option>(); 216 final Set<String> results = new LinkedHashSet<String>(); 217 { 218 add("help", null, "Provide the list of possible options"); 219 } 220 final Option help = charToValues.values().iterator().next(); 221 222 public Options(String mainMessage) { 223 this.mainMessage = (mainMessage.isEmpty() ? "" : mainMessage + "\n") + "Here are the options:\n"; 224 } 225 226 public Options() { 227 this(""); 228 } 229 230 /** 231 * Generate based on class and, optionally, CLDRTool annotation 232 * @param forClass 233 */ 234 public Options(Class<?> forClass) { 235 this(forClass.getSimpleName() + ": " + getCLDRToolDescription(forClass)); 236 } 237 238 public Options add(String string, String helpText) { 239 return add(string, string.charAt(0), null, null, helpText); 240 } 241 242 public Options add(String string, String argumentPattern, String helpText) { 243 return add(string, string.charAt(0), argumentPattern, null, helpText); 244 } 245 246 public Options add(String string, Object argumentPattern, String defaultArgument, String helpText) { 247 return add(string, string.charAt(0), argumentPattern, defaultArgument, helpText); 248 } 249 250 public Option add(Enum<?> optionEnumValue, Object argumentPattern, String defaultArgument, String helpText) { 251 add(optionEnumValue, optionEnumValue.name(), optionEnumValue.name().charAt(0), argumentPattern, 252 defaultArgument, helpText); 253 return get(optionEnumValue.name()); 254 // TODO cleanup 255 } 256 257 public Options add(String string, Character flag, Object argumentPattern, String defaultArgument, 258 String helpText) { 259 return add(null, string, flag, argumentPattern, defaultArgument, helpText); 260 } 261 262 public Options add(Enum<?> optionEnumValue, String string, Character flag, Object argumentPattern, 263 String defaultArgument, String helpText) { 264 Option option = new Option(optionEnumValue, string, flag, argumentPattern, defaultArgument, helpText); 265 return add(optionEnumValue, option); 266 } 267 268 public Options add(Enum<?> optionEnumValue, Option option) { 269 if (stringToValues.containsKey(option.tag)) { 270 throw new IllegalArgumentException("Duplicate tag <" + option.tag + "> with " + stringToValues.get(option.tag)); 271 } 272 if (charToValues.containsKey(option.flag)) { 273 throw new IllegalArgumentException("Duplicate tag <" + option.tag + ", " + option.flag + "> with " 274 + charToValues.get(option.flag)); 275 } 276 stringToValues.put(option.tag, option); 277 charToValues.put(option.flag, option); 278 if (optionEnumValue != null) { 279 enumToValues.put(optionEnumValue, option); 280 } 281 return this; 282 } 283 284 public Set<String> parse(Enum<?> enumOption, String[] args, boolean showArguments) { 285 return parse(args, showArguments); 286 } 287 288 public Set<String> parse(String[] args, boolean showArguments) { 289 results.clear(); 290 for (Option option : charToValues.values()) { 291 option.clear(); 292 } 293 int errorCount = 0; 294 boolean needHelp = false; 295 for (int i = 0; i < args.length; ++i) { 296 String arg = args[i]; 297 if (!arg.startsWith("-")) { 298 results.add(arg); 299 continue; 300 } 301 // can be of the form -fparam or -f param or --file param 302 boolean isStringOption = arg.startsWith("--"); 303 String value = null; 304 Option option; 305 if (isStringOption) { 306 arg = arg.substring(2); 307 int equalsPos = arg.indexOf('='); 308 if (equalsPos > -1) { 309 value = arg.substring(equalsPos + 1); 310 arg = arg.substring(0, equalsPos); 311 } 312 option = stringToValues.get(arg); 313 } else { // starts with single - 314 if (arg.length() > 2) { 315 value = arg.substring(2); 316 } 317 arg = arg.substring(1); 318 option = charToValues.get(arg.charAt(0)); 319 } 320 boolean tookExtraArgument = false; 321 if (value == null) { 322 value = i < args.length - 1 ? args[i + 1] : null; 323 if (value != null && value.startsWith("-")) { 324 value = null; 325 } 326 if (value != null) { 327 ++i; 328 tookExtraArgument = true; 329 } 330 } 331 if (option == null) { 332 ++errorCount; 333 System.out.println("#Unknown flag: " + arg); 334 } else { 335 MatchResult matches = option.matches(value); 336 if (tookExtraArgument && (matches == MatchResult.noValue || matches == MatchResult.noValueError)) { 337 --i; 338 } 339 if (option == help) { 340 needHelp = true; 341 } 342 } 343 } 344 // clean up defaults 345 for (Option option : stringToValues.values()) { 346 if (!option.doesOccur && option.defaultArgument != null) { 347 option.value = option.defaultArgument; 348 // option.implicitValue = true; 349 } 350 } 351 352 if (errorCount > 0) { 353 System.err.println("Invalid Option - Choices are:"); 354 System.err.println(getHelp()); 355 System.exit(1); 356 } else if (needHelp) { 357 System.err.println(getHelp()); 358 System.exit(1); 359 } else if (showArguments) { 360 System.out.println(Arrays.asList(args)); 361 for (Option option : stringToValues.values()) { 362 if (!option.doesOccur && option.value == null) { 363 continue; 364 } 365 System.out.println("#-" + option.flag 366 + "\t" + option.tag 367 + (option.doesOccur ? "\t\t" : "\t\t") + option.value); 368 } 369 } 370 return results; 371 } 372 373 private String getHelp() { 374 StringBuilder buffer = new StringBuilder(mainMessage); 375 boolean first = true; 376 for (Option option : stringToValues.values()) { 377 if (first) { 378 first = false; 379 } else { 380 buffer.append('\n'); 381 } 382 buffer.append(option); 383 } 384 return buffer.toString(); 385 } 386 387 @Override 388 public Iterator<Option> iterator() { 389 return stringToValues.values().iterator(); 390 } 391 392 public Option get(String string) { 393 Option result = stringToValues.get(string); 394 if (result == null) { 395 throw new IllegalArgumentException("Unknown option: " + string); 396 } 397 return result; 398 } 399 400 public Option get(Enum<?> enumOption) { 401 Option result = enumToValues.get(enumOption); 402 if (result == null) { 403 throw new IllegalArgumentException("Unknown option: " + enumOption); 404 } 405 return result; 406 } 407 408 } 409 410 private enum Test { 411 A, B, C 412 } 413 414 final static Options myOptions = new Options() 415 .add("file", ".*", "Filter the information based on file name, using a regex argument") 416 .add("path", ".*", "default-path", "Filter the information based on path name, using a regex argument") 417 .add("content", ".*", "Filter the information based on content name, using a regex argument") 418 .add("gorp", null, null, "Gorp") 419 .add("enum", Test.class, null, "enum check") 420 .add("regex", "a*", null, "Gorp"); 421 422 public static void main(String[] args) { 423 if (args.length == 0) { 424 args = "foo -fen.xml -c a* --path bar -g b -r aaa -e B".split("\\s+"); 425 } 426 myOptions.parse(args, true); 427 428 for (Option option : myOptions) { 429 System.out.println("#" + option.getTag() + "\t" + option.doesOccur() + "\t" + option.getValue() + "\t" 430 + option.getHelpString()); 431 } 432 Option option = myOptions.get("file"); 433 System.out.println("\n#" + option.doesOccur() + "\t" + option.getValue() + "\t" + option); 434 } 435 436 /** 437 * Helper function 438 * @param forClass 439 * @return 440 */ 441 private static String getCLDRToolDescription(Class<?> forClass) { 442 CLDRTool cldrTool = forClass.getAnnotation(CLDRTool.class); 443 if (cldrTool != null) { 444 return cldrTool.description(); 445 } else { 446 return "(no @CLDRTool annotation)"; 447 } 448 } 449 450 public String getDefaultArgument() { 451 return defaultArgument; 452 } 453 454 } 455