xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/tool/Option.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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