• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
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(), (Character) (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(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 
toString()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 
matches(String inputValue)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 
Options(String mainMessage)222         public Options(String mainMessage) {
223             this.mainMessage = (mainMessage.isEmpty() ? "" : mainMessage + "\n") + "Here are the options:\n";
224         }
225 
Options()226         public Options() {
227             this("");
228         }
229 
230         /**
231          * Generate based on class and, optionally, CLDRTool annotation
232          * @param forClass
233          */
Options(Class<?> forClass)234         public Options(Class<?> forClass) {
235             this(forClass.getSimpleName() + ": " + getCLDRToolDescription(forClass));
236         }
237 
add(String string, String helpText)238         public Options add(String string, String helpText) {
239             return add(string, string.charAt(0), null, null, helpText);
240         }
241 
add(String string, String argumentPattern, String helpText)242         public Options add(String string, String argumentPattern, String helpText) {
243             return add(string, string.charAt(0), argumentPattern, null, helpText);
244         }
245 
add(String string, Object argumentPattern, String defaultArgument, String helpText)246         public Options add(String string, Object argumentPattern, String defaultArgument, String helpText) {
247             return add(string, string.charAt(0), argumentPattern, defaultArgument, helpText);
248         }
249 
add(Enum<?> optionEnumValue, Object argumentPattern, String defaultArgument, String helpText)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 
add(String string, Character flag, Object argumentPattern, String defaultArgument, String helpText)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 
add(Enum<?> optionEnumValue, String string, Character flag, Object argumentPattern, String defaultArgument, String helpText)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 
add(Enum<?> optionEnumValue, Option option)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 
parse(Enum<?> enumOption, String[] args, boolean showArguments)284         public Set<String> parse(Enum<?> enumOption, String[] args, boolean showArguments) {
285             return parse(args, showArguments);
286         }
287 
parse(String[] args, boolean showArguments)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 
getHelp()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
iterator()388         public Iterator<Option> iterator() {
389             return stringToValues.values().iterator();
390         }
391 
get(String string)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 
get(Enum<?> enumOption)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 
main(String[] args)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      */
getCLDRToolDescription(Class<?> forClass)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 
getDefaultArgument()450     public String getDefaultArgument() {
451         return defaultArgument;
452     }
453 
454 }
455