| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tradefed.config; |
| |
| import com.android.ddmlib.Log; |
| import com.android.tradefed.util.ArrayUtil; |
| |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| /** |
| * Populates {@link Option} fields from parsed command line arguments. |
| * <p/> |
| * Strings in the passed-in String[] are parsed left-to-right. Each String is classified as a short |
| * option (such as "-v"), a long option (such as "--verbose"), an argument to an option (such as |
| * "out.txt" in "-f out.txt"), or a non-option positional argument. |
| * <p/> |
| * Each option argument must map to one or more {@link Option} fields. A long option maps to the |
| * {@link Option#name()}, and a short option maps to {@link Option#shortName()}. Each |
| * {@link Option#name()} and {@link Option#shortName()} must be unique with respect to all other |
| * {@link Option} fields within the same object. |
| * <p/> |
| * A single option argument can get mapped to multiple {@link Option} fields with the same name |
| * across multiple objects. {@link Option} arguments can be namespaced to uniquely refer to an |
| * {@link Option} field within a single object using that object's full class name or its |
| * {@link OptionClass#alias()} value separated by ':'. ie |
| * |
| * <pre> |
| * --classname:optionname optionvalue or |
| * --optionclassalias:optionname optionvalue. |
| * </pre> |
| * <p/> |
| * A simple short option is a "-" followed by a short option character. If the option requires an |
| * argument (which is true of any non-boolean option), it may be written as a separate parameter, |
| * but need not be. That is, "-f out.txt" and "-fout.txt" are both acceptable. |
| * <p/> |
| * It is possible to specify multiple short options after a single "-" as long as all (except |
| * possibly the last) do not require arguments. |
| * <p/> |
| * A long option begins with "--" followed by several characters. If the option requires an |
| * argument, it may be written directly after the option name, separated by "=", or as the next |
| * argument. (That is, "--file=out.txt" or "--file out.txt".) |
| * <p/> |
| * A boolean long option '--name' automatically gets a '--no-name' companion. Given an option |
| * "--flag", then, "--flag", "--no-flag", "--flag=true" and "--flag=false" are all valid, though |
| * neither "--flag true" nor "--flag false" are allowed (since "--flag" by itself is sufficient, the |
| * following "true" or "false" is interpreted separately). You can use "yes" and "no" as synonyms |
| * for "true" and "false". |
| * <p/> |
| * Each String not starting with a "-" and not a required argument of a previous option is a |
| * non-option positional argument, as are all successive Strings. Each String after a "--" is a |
| * non-option positional argument. |
| * <p/> |
| * The fields corresponding to options are updated as their options are processed. Any remaining |
| * positional arguments are returned as a List<String>. |
| * <p/> |
| * Here's a simple example: |
| * <p/> |
| * |
| * <pre> |
| * // Non-@Option fields will be ignored. |
| * class Options { |
| * @Option(name = "quiet", shortName = 'q') |
| * boolean quiet = false; |
| * |
| * // Here the user can use --no-color. |
| * @Option(name = "color") |
| * boolean color = true; |
| * |
| * @Option(name = "mode", shortName = 'm') |
| * String mode = "standard; // Supply a default just by setting the field. |
| * |
| * @Option(name = "port", shortName = 'p') |
| * int portNumber = 8888; |
| * |
| * // There's no need to offer a short name for rarely-used options. |
| * @Option(name = "timeout" ) |
| * double timeout = 1.0; |
| * |
| * @Option(name = "output-file", shortName = 'o' }) |
| * File output; |
| * |
| * // Multiple options are added to the collection. |
| * // The collection field itself must be non-null. |
| * @Option(name = "input-file", shortName = 'i') |
| * List<File> inputs = new ArrayList<File>(); |
| * |
| * } |
| * |
| * Options options = new Options(); |
| * List<String> posArgs = new OptionParser(options).parse("--input-file", "/tmp/file1.txt"); |
| * for (File inputFile : options.inputs) { |
| * if (!options.quiet) { |
| * ... |
| * } |
| * ... |
| * |
| * } |
| * |
| * </pre> |
| * |
| * See also: |
| * <ul> |
| * <li>the getopt(1) man page |
| * <li>Python's "optparse" module (http://docs.python.org/library/optparse.html) |
| * <li>the POSIX "Utility Syntax Guidelines" |
| * (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02) |
| * <li>the GNU "Standards for Command Line Interfaces" |
| * (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces) |
| * </ul> |
| * |
| * @see {@link OptionSetter} |
| */ |
| public class ArgsOptionParser extends OptionSetter { |
| private static final String LOG_TAG = "ArgsOptionParser"; |
| |
| static final String SHORT_NAME_PREFIX = "-"; |
| static final String OPTION_NAME_PREFIX = "--"; |
| |
| /** the amount to indent an option field's description when displaying help */ |
| private static final int OPTION_DESCRIPTION_INDENT = 25; |
| |
| /** |
| * Creates a {@link ArgsOptionParser} for a collection of objects. |
| * |
| * @param optionSource the config objects. |
| * @throws ConfigurationException if config objects is improperly configured. |
| */ |
| public ArgsOptionParser(Collection<Object> optionSources) throws ConfigurationException { |
| super(optionSources); |
| } |
| |
| /** |
| * Creates a {@link ArgsOptionParser} for one or more objects. |
| * |
| * @param optionSource the config objects. |
| * @throws ConfigurationException if config objects is improperly configured. |
| */ |
| public ArgsOptionParser(Object... optionSources) throws ConfigurationException { |
| super(optionSources); |
| } |
| |
| /** |
| * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource' |
| * provided to the constructor. |
| * |
| * @returns a {@link List} of the positional arguments left over after processing all options. |
| * @throws ConfigurationException if error occurred parsing the arguments. |
| */ |
| public List<String> parse(String... args) throws ConfigurationException { |
| return parse(Arrays.asList(args)); |
| } |
| |
| /** |
| * Alternate {@link #parse(String... args)} method that takes a {@link List} of arguments |
| * |
| * @returns a {@link List} of the positional arguments left over after processing all options. |
| * @throws ConfigurationException if error occurred parsing the arguments. |
| */ |
| public List<String> parse(List<String> args) throws ConfigurationException { |
| return parseOptions(args.listIterator()); |
| } |
| |
| private List<String> parseOptions(ListIterator<String> args) throws ConfigurationException { |
| final List<String> leftovers = new ArrayList<String>(); |
| |
| // Scan 'args'. |
| while (args.hasNext()) { |
| final String arg = args.next(); |
| if (arg.equals(OPTION_NAME_PREFIX)) { |
| // "--" marks the end of options and the beginning of positional arguments. |
| break; |
| } else if (arg.startsWith(OPTION_NAME_PREFIX)) { |
| // A long option. |
| parseLongOption(arg, args); |
| } else if (arg.startsWith(SHORT_NAME_PREFIX)) { |
| // A short option. |
| parseGroupedShortOptions(arg, args); |
| } else { |
| // The first non-option marks the end of options. |
| leftovers.add(arg); |
| break; |
| } |
| } |
| |
| // Make sure that all mandatory options have been specified |
| List<String> missingOptions = new ArrayList(getUnsetMandatoryOptions()); |
| if (!missingOptions.isEmpty()) { |
| throw new ConfigurationException(String.format("Found missing mandatory options: %s", |
| ArrayUtil.join(", ", missingOptions))); |
| } |
| |
| // Package up the leftovers. |
| while (args.hasNext()) { |
| leftovers.add(args.next()); |
| } |
| return leftovers; |
| } |
| |
| private void parseLongOption(String arg, ListIterator<String> args) |
| throws ConfigurationException { |
| // remove prefix to just get name |
| String name = arg.replaceFirst("^" + OPTION_NAME_PREFIX, ""); |
| String key = null; |
| String value = null; |
| |
| // Support "--name=value" as well as "--name value". |
| final int equalsIndex = name.indexOf('='); |
| if (equalsIndex != -1) { |
| value = name.substring(equalsIndex + 1); |
| name = name.substring(0, equalsIndex); |
| } |
| |
| if (value == null) { |
| if (isBooleanOption(name)) { |
| value = name.startsWith(BOOL_FALSE_PREFIX) ? "false" : "true"; |
| } else if (isMapOption(name)) { |
| key = grabNextValue(args, name, "for its key"); |
| value = grabNextValue(args, name, "for its value"); |
| } else { |
| value = grabNextValue(args, name); |
| } |
| } |
| if (isMapOption(name)) { |
| setOptionMapValue(name, key, value); |
| } else { |
| setOptionValue(name, value); |
| } |
| } |
| |
| // Given boolean options a and b, and non-boolean option f, we want to allow: |
| // -ab |
| // -abf out.txt |
| // -abfout.txt |
| // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids |
| // it.) |
| private void parseGroupedShortOptions(String arg, ListIterator<String> args) |
| throws ConfigurationException { |
| for (int i = 1; i < arg.length(); ++i) { |
| final String name = String.valueOf(arg.charAt(i)); |
| String value; |
| if (isBooleanOption(name)) { |
| value = "true"; |
| } else { |
| // We need a value. If there's anything left, we take the rest of this |
| // "short option". |
| if (i + 1 < arg.length()) { |
| value = arg.substring(i + 1); |
| i = arg.length() - 1; |
| } else { |
| value = grabNextValue(args, name); |
| } |
| } |
| setOptionValue(name, value); |
| } |
| } |
| |
| /** |
| * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error |
| * message. |
| * |
| * @param args the arg iterator |
| * @param name the name of current argument |
| * @throws ConfigurationException if no argument is present |
| * |
| * @returns the next element |
| */ |
| private String grabNextValue(ListIterator<String> args, String name) |
| throws ConfigurationException { |
| return grabNextValue(args, name, ""); |
| } |
| |
| /** |
| * Returns the next element of 'args' if there is one. Uses 'name' to construct a helpful error |
| * message. |
| * |
| * @param args the arg iterator |
| * @param name the name of current argument |
| * @param detail a string to append to the ConfigurationException message, if one is thrown |
| * @throws ConfigurationException if no argument is present |
| * |
| * @returns the next element |
| */ |
| private String grabNextValue(ListIterator<String> args, String name, String detail) |
| throws ConfigurationException { |
| if (!args.hasNext()) { |
| String type = getTypeForOption(name); |
| throw new ConfigurationException(String.format("option '%s' requires a '%s' argument%s", |
| name, type, detail)); |
| } |
| return args.next(); |
| } |
| |
| /** |
| * Output help text for all {@link Option} fields in <param>optionObject</param>. |
| * <p/> |
| * The help text for each option will be in the following format |
| * <pre> |
| * [-option_shortname, --option_name] [option_description] Default: |
| * [current option field's value in optionObject] |
| * </pre> |
| * The 'Default..." text will be omitted if the option field is null or empty. |
| * |
| * @param importantOnly if <code>true</code> only print help for the important options |
| * @param optionObject the object to print help text for |
| * @return a String containing user-friendly help text for all Option fields |
| */ |
| public static String getOptionHelp(boolean importantOnly, Object optionObject) { |
| StringBuilder out = new StringBuilder(); |
| Collection<Field> optionFields = OptionSetter.getOptionFieldsForClass( |
| optionObject.getClass()); |
| String eol = System.getProperty("line.separator"); |
| for (Field field : optionFields) { |
| final Option option = field.getAnnotation(Option.class); |
| String defaultValue = OptionSetter.getFieldValueAsString(field, optionObject); |
| String optionNameHelp = buildOptionNameHelp(field, option); |
| if (shouldOutputHelpForOption(importantOnly, option, defaultValue)) { |
| out.append(optionNameHelp); |
| // insert appropriate whitespace between the name help and the description, to |
| // ensure consistent alignment |
| int wsChars = 0; |
| if (optionNameHelp.length() >= OPTION_DESCRIPTION_INDENT) { |
| // name help is too long, break description onto next line |
| out.append(eol); |
| wsChars = OPTION_DESCRIPTION_INDENT; |
| } else { |
| // insert enough whitespace so option.description starts at |
| // OPTION_DESCRIPTION_INDENT |
| wsChars = OPTION_DESCRIPTION_INDENT - optionNameHelp.length(); |
| } |
| for (int i = 0; i < wsChars; ++i) { |
| out.append(' '); |
| } |
| out.append(option.description()); |
| out.append(getDefaultValueHelp(defaultValue)); |
| out.append(OptionSetter.getEnumFieldValuesAsString(field)); |
| out.append(eol); |
| } |
| } |
| return out.toString(); |
| } |
| |
| /** |
| * Determine if help for given option should be displayed. |
| * |
| * @param importantOnly |
| * @param option |
| * @param defaultValue |
| * @return <code>true</code> if help for option should be displayed |
| */ |
| private static boolean shouldOutputHelpForOption(boolean importantOnly, Option option, |
| String defaultValue) { |
| if (!importantOnly) { |
| return true; |
| } |
| switch (option.importance()) { |
| case NEVER: |
| return false; |
| case IF_UNSET: |
| return defaultValue == null; |
| case ALWAYS: |
| return true; |
| } |
| Log.e(LOG_TAG, String.format("Unrecognized importance setting '%s'", |
| option.importance().toString())); |
| return false; |
| } |
| |
| /** |
| * Builds the 'name' portion of the help text for the given option field |
| * |
| * @param field |
| * @param option |
| * @return the help text that describes the option flags |
| */ |
| private static String buildOptionNameHelp(Field field, final Option option) { |
| StringBuilder optionNameBuilder = new StringBuilder(); |
| optionNameBuilder.append(" "); |
| if (option.shortName() != Option.NO_SHORT_NAME) { |
| optionNameBuilder.append(SHORT_NAME_PREFIX); |
| optionNameBuilder.append(option.shortName()); |
| optionNameBuilder.append(", "); |
| } |
| optionNameBuilder.append(OPTION_NAME_PREFIX); |
| try { |
| if (OptionSetter.isBooleanField(field)) { |
| optionNameBuilder.append("[no-]"); |
| } |
| } catch (ConfigurationException e) { |
| // ignore |
| } |
| optionNameBuilder.append(option.name()); |
| return optionNameBuilder.toString(); |
| } |
| |
| /** |
| * Returns the help text describing the given default value |
| * |
| * @param defaultValue the default value |
| * @return the help text, or an empty {@link String} if <param>field</param> has no value |
| */ |
| private static String getDefaultValueHelp(String defaultValue) { |
| if (defaultValue == null) { |
| return ""; |
| } else { |
| return String.format(" Default: %s.", defaultValue); |
| } |
| } |
| } |