• 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.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