Home | History | Annotate | Download | only in tool
      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