Home | History | Annotate | Download | only in config
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.loganalysis.util.config;
     17 
     18 import com.android.loganalysis.util.ArrayUtil;
     19 
     20 import java.lang.reflect.Field;
     21 import java.util.ArrayList;
     22 import java.util.Arrays;
     23 import java.util.Collection;
     24 import java.util.List;
     25 import java.util.ListIterator;
     26 
     27 /**
     28  * Populates {@link Option} fields from parsed command line arguments.
     29  * <p/>
     30  * Strings in the passed-in String[] are parsed left-to-right. Each String is classified as a short
     31  * option (such as "-v"), a long option (such as "--verbose"), an argument to an option (such as
     32  * "out.txt" in "-f out.txt"), or a non-option positional argument.
     33  * <p/>
     34  * Each option argument must map to one or more {@link Option} fields. A long option maps to the
     35  * {@link Option#name}, and a short option maps to {@link Option#shortName}. Each
     36  * {@link Option#name()} and {@link Option#shortName()} must be unique with respect to all other
     37  * {@link Option} fields within the same object.
     38  * <p/>
     39  * A single option argument can get mapped to multiple {@link Option} fields with the same name
     40  * across multiple objects. {@link Option} arguments can be namespaced to uniquely refer to an
     41  * {@link Option} field within a single object using that object's full class name or its
     42  * {@link OptionClass#alias()} value separated by ':'. ie
     43  *
     44  * <pre>
     45  * --classname:optionname optionvalue or
     46  * --optionclassalias:optionname optionvalue.
     47  * </pre>
     48  * <p/>
     49  * A simple short option is a "-" followed by a short option character. If the option requires an
     50  * argument (which is true of any non-boolean option), it may be written as a separate parameter,
     51  * but need not be. That is, "-f out.txt" and "-fout.txt" are both acceptable.
     52  * <p/>
     53  * It is possible to specify multiple short options after a single "-" as long as all (except
     54  * possibly the last) do not require arguments.
     55  * <p/>
     56  * A long option begins with "--" followed by several characters. If the option requires an
     57  * argument, it may be written directly after the option name, separated by "=", or as the next
     58  * argument. (That is, "--file=out.txt" or "--file out.txt".)
     59  * <p/>
     60  * A boolean long option '--name' automatically gets a '--no-name' companion. Given an option
     61  * "--flag", then, "--flag", "--no-flag", "--flag=true" and "--flag=false" are all valid, though
     62  * neither "--flag true" nor "--flag false" are allowed (since "--flag" by itself is sufficient, the
     63  * following "true" or "false" is interpreted separately). You can use "yes" and "no" as synonyms
     64  * for "true" and "false".
     65  * <p/>
     66  * Each String not starting with a "-" and not a required argument of a previous option is a
     67  * non-option positional argument, as are all successive Strings. Each String after a "--" is a
     68  * non-option positional argument.
     69  * <p/>
     70  * The fields corresponding to options are updated as their options are processed. Any remaining
     71  * positional arguments are returned as a List<String>.
     72  * <p/>
     73  * Here's a simple example:
     74  * <p/>
     75  *
     76  * <pre>
     77  * // Non-&#64;Option fields will be ignored.
     78  * class Options {
     79  *     &#64;Option(name = "quiet", shortName = 'q')
     80  *     boolean quiet = false;
     81  *
     82  *     // Here the user can use --no-color.
     83  *     &#64;Option(name = "color")
     84  *     boolean color = true;
     85  *
     86  *     &#64;Option(name = "mode", shortName = 'm')
     87  *     String mode = "standard; // Supply a default just by setting the field.
     88  *
     89  *     &#64;Option(name = "port", shortName = 'p')
     90  *     int portNumber = 8888;
     91  *
     92  *     // There's no need to offer a short name for rarely-used options.
     93  *     &#64;Option(name = "timeout" )
     94  *     double timeout = 1.0;
     95  *
     96  *     &#64;Option(name = "output-file", shortName = 'o' })
     97  *     File output;
     98  *
     99  *     // Multiple options are added to the collection.
    100  *     // The collection field itself must be non-null.
    101  *     &#64;Option(name = "input-file", shortName = 'i')
    102  *     List<File> inputs = new ArrayList<File>();
    103  *
    104  * }
    105  *
    106  * Options options = new Options();
    107  * List<String> posArgs = new OptionParser(options).parse("--input-file", "/tmp/file1.txt");
    108  * for (File inputFile : options.inputs) {
    109  *     if (!options.quiet) {
    110  *        ...
    111  *     }
    112  *     ...
    113  *
    114  * }
    115  *
    116  * </pre>
    117  *
    118  * See also:
    119  * <ul>
    120  * <li>the getopt(1) man page
    121  * <li>Python's "optparse" module (http://docs.python.org/library/optparse.html)
    122  * <li>the POSIX "Utility Syntax Guidelines"
    123  * (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02)
    124  * <li>the GNU "Standards for Command Line Interfaces"
    125  * (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces)
    126  * </ul>
    127  *
    128  * @see {@link OptionSetter}
    129  */
    130 //TODO: Use libTF once this is copied over.
    131 public class ArgsOptionParser extends OptionSetter {
    132 
    133     static final String SHORT_NAME_PREFIX = "-";
    134     static final String OPTION_NAME_PREFIX = "--";
    135 
    136     /** the amount to indent an option field's description when displaying help */
    137     private static final int OPTION_DESCRIPTION_INDENT = 25;
    138 
    139     /**
    140      * Creates a {@link ArgsOptionParser} for a collection of objects.
    141      *
    142      * @param optionSources the config objects.
    143      * @throws ConfigurationException if config objects is improperly configured.
    144      */
    145     public ArgsOptionParser(Collection<Object> optionSources) throws ConfigurationException {
    146         super(optionSources);
    147     }
    148 
    149     /**
    150      * Creates a {@link ArgsOptionParser} for one or more objects.
    151      *
    152      * @param optionSources the config objects.
    153      * @throws ConfigurationException if config objects is improperly configured.
    154      */
    155     public ArgsOptionParser(Object... optionSources) throws ConfigurationException {
    156         super(optionSources);
    157     }
    158 
    159     /**
    160      * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource'
    161      * provided to the constructor.
    162      *
    163      * @return a {@link List} of the positional arguments left over after processing all options.
    164      * @throws ConfigurationException if error occurred parsing the arguments.
    165      */
    166     public List<String> parse(String... args) throws ConfigurationException {
    167         return parse(Arrays.asList(args));
    168     }
    169 
    170     /**
    171      * Alternate {@link #parse(String... args)} method that takes a {@link List} of arguments
    172      *
    173      * @return a {@link List} of the positional arguments left over after processing all options.
    174      * @throws ConfigurationException if error occurred parsing the arguments.
    175      */
    176     public List<String> parse(List<String> args) throws ConfigurationException {
    177         return parseOptions(args.listIterator());
    178     }
    179 
    180     /**
    181      * Validates that all fields marked as {@link Option#mandatory()} have been set.
    182      * @throws ConfigurationException
    183      */
    184     public void validateMandatoryOptions() throws ConfigurationException {
    185         // Make sure that all mandatory options have been specified
    186         List<String> missingOptions = new ArrayList<String>(getUnsetMandatoryOptions());
    187         if (!missingOptions.isEmpty()) {
    188             throw new ConfigurationException(String.format("Found missing mandatory options: %s",
    189                     ArrayUtil.join(", ", missingOptions)));
    190         }
    191     }
    192 
    193     private List<String> parseOptions(ListIterator<String> args) throws ConfigurationException {
    194         final List<String> leftovers = new ArrayList<String>();
    195 
    196         // Scan 'args'.
    197         while (args.hasNext()) {
    198             final String arg = args.next();
    199             if (arg.equals(OPTION_NAME_PREFIX)) {
    200                 // "--" marks the end of options and the beginning of positional arguments.
    201                 break;
    202             } else if (arg.startsWith(OPTION_NAME_PREFIX)) {
    203                 // A long option.
    204                 parseLongOption(arg, args);
    205             } else if (arg.startsWith(SHORT_NAME_PREFIX)) {
    206                 // A short option.
    207                 parseGroupedShortOptions(arg, args);
    208             } else {
    209                 // The first non-option marks the end of options.
    210                 leftovers.add(arg);
    211                 break;
    212             }
    213         }
    214 
    215         // Package up the leftovers.
    216         while (args.hasNext()) {
    217             leftovers.add(args.next());
    218         }
    219         return leftovers;
    220     }
    221 
    222     private void parseLongOption(String arg, ListIterator<String> args)
    223             throws ConfigurationException {
    224         // remove prefix to just get name
    225         String name = arg.replaceFirst("^" + OPTION_NAME_PREFIX, "");
    226         String key = null;
    227         String value = null;
    228 
    229         // Support "--name=value" as well as "--name value".
    230         final int equalsIndex = name.indexOf('=');
    231         if (equalsIndex != -1) {
    232             value = name.substring(equalsIndex + 1);
    233             name = name.substring(0, equalsIndex);
    234         }
    235 
    236         if (value == null) {
    237             if (isBooleanOption(name)) {
    238                 int idx = name.indexOf(NAMESPACE_SEPARATOR);
    239                 value = name.startsWith(BOOL_FALSE_PREFIX, idx + 1) ? "false" : "true";
    240             } else if (isMapOption(name)) {
    241                 key = grabNextValue(args, name, "for its key");
    242                 value = grabNextValue(args, name, "for its value");
    243             } else {
    244                 value = grabNextValue(args, name);
    245             }
    246         }
    247         if (isMapOption(name)) {
    248             setOptionMapValue(name, key, value);
    249         } else {
    250             setOptionValue(name, value);
    251         }
    252     }
    253 
    254     // Given boolean options a and b, and non-boolean option f, we want to allow:
    255     // -ab
    256     // -abf out.txt
    257     // -abfout.txt
    258     // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids
    259     // it.)
    260     private void parseGroupedShortOptions(String arg, ListIterator<String> args)
    261             throws ConfigurationException {
    262         for (int i = 1; i < arg.length(); ++i) {
    263             final String name = String.valueOf(arg.charAt(i));
    264             String value;
    265             if (isBooleanOption(name)) {
    266                 value = "true";
    267             } else {
    268                 // We need a value. If there's anything left, we take the rest of this
    269                 // "short option".
    270                 if (i + 1 < arg.length()) {
    271                     value = arg.substring(i + 1);
    272                     i = arg.length() - 1;
    273                 } else {
    274                     value = grabNextValue(args, name);
    275                 }
    276             }
    277             setOptionValue(name, value);
    278         }
    279     }
    280 
    281     /**
    282      * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error
    283      * message.
    284      *
    285      * @param args the arg iterator
    286      * @param name the name of current argument
    287      * @throws ConfigurationException if no argument is present
    288      *
    289      * @returns the next element
    290      */
    291     private String grabNextValue(ListIterator<String> args, String name)
    292             throws ConfigurationException {
    293         return grabNextValue(args, name, "");
    294     }
    295 
    296     /**
    297      * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error
    298      * message.
    299      *
    300      * @param args the arg iterator
    301      * @param name the name of current argument
    302      * @param detail a string to append to the ConfigurationException message, if one is thrown
    303      * @throws ConfigurationException if no argument is present
    304      *
    305      * @returns the next element
    306      */
    307     private String grabNextValue(ListIterator<String> args, String name, String detail)
    308             throws ConfigurationException {
    309         if (!args.hasNext()) {
    310             String type = getTypeForOption(name);
    311             throw new ConfigurationException(String.format("option '%s' requires a '%s' argument%s",
    312                     name, type, detail));
    313         }
    314         return args.next();
    315     }
    316 
    317     /**
    318      * Output help text for all {@link Option} fields in <param>optionObject</param>.
    319      * <p/>
    320      * The help text for each option will be in the following format
    321      * <pre>
    322      *   [-option_shortname, --option_name]          [option_description] Default:
    323      *   [current option field's value in optionObject]
    324      * </pre>
    325      * The 'Default..." text will be omitted if the option field is null or empty.
    326      *
    327      * @param importantOnly if <code>true</code> only print help for the important options
    328      * @param optionObject the object to print help text for
    329      * @return a String containing user-friendly help text for all Option fields
    330      */
    331     public static String getOptionHelp(boolean importantOnly, Object optionObject) {
    332         StringBuilder out = new StringBuilder();
    333         Collection<Field> optionFields = OptionSetter.getOptionFieldsForClass(
    334                 optionObject.getClass());
    335         String eol = System.getProperty("line.separator");
    336         for (Field field : optionFields) {
    337             final Option option = field.getAnnotation(Option.class);
    338             String defaultValue = OptionSetter.getFieldValueAsString(field, optionObject);
    339             String optionNameHelp = buildOptionNameHelp(field, option);
    340             if (shouldOutputHelpForOption(importantOnly, option, defaultValue)) {
    341                 out.append(optionNameHelp);
    342                 // insert appropriate whitespace between the name help and the description, to
    343                 // ensure consistent alignment
    344                 int wsChars = 0;
    345                 if (optionNameHelp.length() >= OPTION_DESCRIPTION_INDENT) {
    346                     // name help is too long, break description onto next line
    347                     out.append(eol);
    348                     wsChars = OPTION_DESCRIPTION_INDENT;
    349                 } else {
    350                     // insert enough whitespace so option.description starts at
    351                     // OPTION_DESCRIPTION_INDENT
    352                     wsChars = OPTION_DESCRIPTION_INDENT - optionNameHelp.length();
    353                 }
    354                 for (int i = 0; i < wsChars; ++i) {
    355                     out.append(' ');
    356                 }
    357                 out.append(option.description());
    358                 out.append(getDefaultValueHelp(defaultValue));
    359                 out.append(OptionSetter.getEnumFieldValuesAsString(field));
    360                 out.append(eol);
    361             }
    362         }
    363         return out.toString();
    364     }
    365 
    366     /**
    367      * Determine if help for given option should be displayed.
    368      *
    369      * @param importantOnly
    370      * @param option
    371      * @param defaultValue
    372      * @return <code>true</code> if help for option should be displayed
    373      */
    374     private static boolean shouldOutputHelpForOption(boolean importantOnly, Option option,
    375             String defaultValue) {
    376         if (!importantOnly) {
    377             return true;
    378         }
    379         switch (option.importance()) {
    380             case NEVER:
    381                 return false;
    382             case IF_UNSET:
    383                 return defaultValue == null;
    384             case ALWAYS:
    385                 return true;
    386         }
    387         return false;
    388     }
    389 
    390     /**
    391      * Builds the 'name' portion of the help text for the given option field
    392      *
    393      * @param field
    394      * @param option
    395      * @return the help text that describes the option flags
    396      */
    397     private static String buildOptionNameHelp(Field field, final Option option) {
    398         StringBuilder optionNameBuilder = new StringBuilder();
    399         optionNameBuilder.append("    ");
    400         if (option.shortName() != Option.NO_SHORT_NAME) {
    401             optionNameBuilder.append(SHORT_NAME_PREFIX);
    402             optionNameBuilder.append(option.shortName());
    403             optionNameBuilder.append(", ");
    404         }
    405         optionNameBuilder.append(OPTION_NAME_PREFIX);
    406         try {
    407             if (OptionSetter.isBooleanField(field)) {
    408                 optionNameBuilder.append("[no-]");
    409             }
    410         } catch (ConfigurationException e) {
    411             // ignore
    412         }
    413         optionNameBuilder.append(option.name());
    414         return optionNameBuilder.toString();
    415     }
    416 
    417     /**
    418      * Returns the help text describing the given default value
    419      *
    420      * @param defaultValue the default value
    421      * @return the help text, or an empty {@link String} if <param>field</param> has no value
    422      */
    423     private static String getDefaultValueHelp(String defaultValue) {
    424         if (defaultValue == null) {
    425             return "";
    426         } else {
    427             return String.format(" Default: %s.", defaultValue);
    428         }
    429     }
    430 }
    431