| /* |
| * 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 vogar; |
| |
| import com.google.common.collect.Lists; |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.ParameterizedType; |
| import java.lang.reflect.Type; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import vogar.util.Strings; |
| |
| /** |
| * Parses command line options. |
| * |
| * 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. |
| * |
| * 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. |
| * |
| * It is possible to specify multiple short options after a single "-" |
| * as long as all (except possibly the last) do not require arguments. |
| * |
| * 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".) |
| * |
| * 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". |
| * |
| * 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. |
| * |
| * Parsing of numeric fields such byte, short, int, long, float, and |
| * double fields is supported. This includes both unboxed and boxed |
| * versions (e.g. int vs Integer). If there is a problem parsing the |
| * argument to match the desired type, a runtime exception is thrown. |
| * |
| * File option fields are supported by simply wrapping the string |
| * argument in a File object without testing for the existance of the |
| * file. |
| * |
| * Parameterized Collection fields such as List<File> and Set<String> |
| * are supported as long as the parameter type is otherwise supported |
| * by the option parser. The collection field should be initialized |
| * with an appropriate collection instance. |
| * |
| * Enum types are supported. Input may be in either CONSTANT_CASE or |
| * lower_case. |
| * |
| * The fields corresponding to options are updated as their options |
| * are processed. Any remaining positional arguments are returned as a |
| * List<String>. |
| * |
| * Here's a simple example: |
| * |
| * // This doesn't need to be a separate class, if your application doesn't warrant it. |
| * // Non-@Option fields will be ignored. |
| * class Options { |
| * @Option(names = { "-q", "--quiet" }) |
| * boolean quiet = false; |
| * |
| * // Boolean options require a long name if it's to be possible to explicitly turn them off. |
| * // Here the user can use --no-color. |
| * @Option(names = { "--color" }) |
| * boolean color = true; |
| * |
| * @Option(names = { "-m", "--mode" }) |
| * String mode = "standard; // Supply a default just by setting the field. |
| * |
| * @Option(names = { "-p", "--port" }) |
| * int portNumber = 8888; |
| * |
| * // There's no need to offer a short name for rarely-used options. |
| * @Option(names = { "--timeout" }) |
| * double timeout = 1.0; |
| * |
| * @Option(names = { "-o", "--output-file" }) |
| * File output; |
| * |
| * // Multiple options are added to the collection. |
| * // The collection field itself must be non-null. |
| * @Option(names = { "-i", "--input-file" }) |
| * List<File> inputs = new ArrayList<File>(); |
| * |
| * } |
| * |
| * class Main { |
| * public static void main(String[] args) { |
| * Options options = new Options(); |
| * List<String> inputFilenames = new OptionParser(options).parse(args); |
| * for (String inputFilename : inputFilenames) { |
| * if (!options.quiet) { |
| * ... |
| * } |
| * ... |
| * } |
| * } |
| * } |
| * |
| * See also: |
| * |
| * the getopt(1) man page |
| * Python's "optparse" module (http://docs.python.org/library/optparse.html) |
| * the POSIX "Utility Syntax Guidelines" (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02) |
| * the GNU "Standards for Command Line Interfaces" (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces) |
| */ |
| public class OptionParser { |
| private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>(); |
| static { |
| handlers.put(boolean.class, new BooleanHandler()); |
| handlers.put(Boolean.class, new BooleanHandler()); |
| |
| handlers.put(byte.class, new ByteHandler()); |
| handlers.put(Byte.class, new ByteHandler()); |
| handlers.put(short.class, new ShortHandler()); |
| handlers.put(Short.class, new ShortHandler()); |
| handlers.put(int.class, new IntegerHandler()); |
| handlers.put(Integer.class, new IntegerHandler()); |
| handlers.put(long.class, new LongHandler()); |
| handlers.put(Long.class, new LongHandler()); |
| |
| handlers.put(float.class, new FloatHandler()); |
| handlers.put(Float.class, new FloatHandler()); |
| handlers.put(double.class, new DoubleHandler()); |
| handlers.put(Double.class, new DoubleHandler()); |
| |
| handlers.put(String.class, new StringHandler()); |
| handlers.put(File.class, new FileHandler()); |
| } |
| Handler getHandler(Type type) { |
| if (type instanceof ParameterizedType) { |
| ParameterizedType parameterizedType = (ParameterizedType) type; |
| Class rawClass = (Class<?>) parameterizedType.getRawType(); |
| if (!Collection.class.isAssignableFrom(rawClass)) { |
| throw new RuntimeException("cannot handle non-collection parameterized type " + type); |
| } |
| Type actualType = parameterizedType.getActualTypeArguments()[0]; |
| if (!(actualType instanceof Class)) { |
| throw new RuntimeException("cannot handle nested parameterized type " + type); |
| } |
| return getHandler(actualType); |
| } |
| if (type instanceof Class) { |
| Class<?> classType = (Class) type; |
| if (Collection.class.isAssignableFrom(classType)) { |
| // could handle by just having a default of treating |
| // contents as String but consciously decided this |
| // should be an error |
| throw new RuntimeException( |
| "cannot handle non-parameterized collection " + type + ". " + |
| "use a generic Collection to specify a desired element type"); |
| } |
| if (classType.isEnum()) { |
| return new EnumHandler(classType); |
| } |
| return handlers.get(classType); |
| } |
| throw new RuntimeException("cannot handle unknown field type " + type); |
| } |
| |
| private final Object optionSource; |
| private final HashMap<String, Field> optionMap; |
| private final Map<Field, Object> defaultOptionMap; |
| |
| /** |
| * Constructs a new OptionParser for setting the @Option fields of 'optionSource'. |
| */ |
| public OptionParser(Object optionSource) { |
| this.optionSource = optionSource; |
| this.optionMap = makeOptionMap(); |
| this.defaultOptionMap = new HashMap<Field, Object>(); |
| } |
| |
| public static String[] readFile(File configFile) { |
| if (!configFile.exists()) { |
| return new String[0]; |
| } |
| |
| List<String> configFileLines; |
| try { |
| configFileLines = Strings.readFileLines(configFile); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| |
| List<String> argsList = Lists.newArrayList(); |
| for (String rawLine : configFileLines) { |
| String line = rawLine.trim(); |
| |
| // allow comments and blank lines |
| if (line.startsWith("#") || line.isEmpty()) { |
| continue; |
| } |
| int space = line.indexOf(' '); |
| if (space == -1) { |
| argsList.add(line); |
| } else { |
| argsList.add(line.substring(0, space)); |
| argsList.add(line.substring(space + 1).trim()); |
| } |
| } |
| |
| return argsList.toArray(new String[argsList.size()]); |
| } |
| |
| /** |
| * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource' provided to the constructor. |
| * Returns a list of the positional arguments left over after processing all options. |
| */ |
| public List<String> parse(String[] args) { |
| return parseOptions(Arrays.asList(args).iterator()); |
| } |
| |
| private List<String> parseOptions(Iterator<String> args) { |
| final List<String> leftovers = new ArrayList<String>(); |
| |
| // Scan 'args'. |
| while (args.hasNext()) { |
| final String arg = args.next(); |
| if (arg.equals("--")) { |
| // "--" marks the end of options and the beginning of positional arguments. |
| break; |
| } else if (arg.startsWith("--")) { |
| // A long option. |
| parseLongOption(arg, args); |
| } else if (arg.startsWith("-")) { |
| // A short option. |
| parseGroupedShortOptions(arg, args); |
| } else { |
| // The first non-option marks the end of options. |
| leftovers.add(arg); |
| break; |
| } |
| } |
| |
| // Package up the leftovers. |
| while (args.hasNext()) { |
| leftovers.add(args.next()); |
| } |
| return leftovers; |
| } |
| |
| private Field fieldForArg(String name) { |
| final Field field = optionMap.get(name); |
| if (field == null) { |
| throw new RuntimeException("unrecognized option '" + name + "'"); |
| } |
| return field; |
| } |
| |
| private void parseLongOption(String arg, Iterator<String> args) { |
| String name = arg.replaceFirst("^--no-", "--"); |
| 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); |
| } |
| |
| final Field field = fieldForArg(name); |
| final Handler handler = getHandler(field.getGenericType()); |
| if (value == null) { |
| if (handler.isBoolean()) { |
| value = arg.startsWith("--no-") ? "false" : "true"; |
| } else { |
| value = grabNextValue(args, name, field); |
| } |
| } |
| setValue(field, arg, handler, 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, Iterator<String> args) { |
| for (int i = 1; i < arg.length(); ++i) { |
| final String name = "-" + arg.charAt(i); |
| final Field field = fieldForArg(name); |
| final Handler handler = getHandler(field.getGenericType()); |
| String value; |
| if (handler.isBoolean()) { |
| 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, field); |
| } |
| } |
| setValue(field, arg, handler, value); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private void setValue(Field field, String arg, Handler handler, String valueText) { |
| |
| Object value = handler.translate(valueText); |
| if (value == null) { |
| final String type = field.getType().getSimpleName().toLowerCase(); |
| throw new RuntimeException("couldn't convert '" + valueText + "' to a " + type + " for option '" + arg + "'"); |
| } |
| try { |
| field.setAccessible(true); |
| // record the original value of the field so it can be reset |
| if (!defaultOptionMap.containsKey(field)) { |
| defaultOptionMap.put(field, field.get(optionSource)); |
| } |
| if (Collection.class.isAssignableFrom(field.getType())) { |
| Collection collection = (Collection) field.get(optionSource); |
| collection.add(value); |
| } else { |
| field.set(optionSource, value); |
| } |
| } catch (IllegalAccessException ex) { |
| throw new RuntimeException("internal error", ex); |
| } |
| } |
| |
| /** |
| * Resets optionSource's fields to their defaults |
| */ |
| public void reset() { |
| for (Map.Entry<Field, Object> entry : defaultOptionMap.entrySet()) { |
| try { |
| entry.getKey().set(optionSource, entry.getValue()); |
| } catch (IllegalAccessException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| // Returns the next element of 'args' if there is one. Uses 'name' and 'field' to construct a helpful error message. |
| private String grabNextValue(Iterator<String> args, String name, Field field) { |
| if (!args.hasNext()) { |
| final String type = field.getType().getSimpleName().toLowerCase(); |
| throw new RuntimeException("option '" + name + "' requires a " + type + " argument"); |
| } |
| return args.next(); |
| } |
| |
| // Cache the available options and report any problems with the options themselves right away. |
| private HashMap<String, Field> makeOptionMap() { |
| final HashMap<String, Field> optionMap = new HashMap<String, Field>(); |
| final Class<?> optionClass = optionSource.getClass(); |
| for (Field field : optionClass.getDeclaredFields()) { |
| if (field.isAnnotationPresent(Option.class)) { |
| final Option option = field.getAnnotation(Option.class); |
| final String[] names = option.names(); |
| if (names.length == 0) { |
| throw new RuntimeException("found an @Option with no name!"); |
| } |
| for (String name : names) { |
| if (optionMap.put(name, field) != null) { |
| throw new RuntimeException("found multiple @Options sharing the name '" + name + "'"); |
| } |
| } |
| if (getHandler(field.getGenericType()) == null) { |
| throw new RuntimeException("unsupported @Option field type '" + field.getType() + "'"); |
| } |
| } |
| } |
| return optionMap; |
| } |
| |
| static abstract class Handler { |
| // Only BooleanHandler should ever override this. |
| boolean isBoolean() { |
| return false; |
| } |
| |
| /** |
| * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'. |
| * Returns null on failure. |
| */ |
| abstract Object translate(String valueText); |
| } |
| |
| static class BooleanHandler extends Handler { |
| @Override boolean isBoolean() { |
| return true; |
| } |
| |
| Object translate(String valueText) { |
| if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) { |
| return Boolean.TRUE; |
| } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) { |
| return Boolean.FALSE; |
| } |
| return null; |
| } |
| } |
| |
| static class ByteHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Byte.parseByte(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class ShortHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Short.parseShort(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class IntegerHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Integer.parseInt(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class LongHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Long.parseLong(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class FloatHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Float.parseFloat(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class DoubleHandler extends Handler { |
| Object translate(String valueText) { |
| try { |
| return Double.parseDouble(valueText); |
| } catch (NumberFormatException ex) { |
| return null; |
| } |
| } |
| } |
| |
| static class StringHandler extends Handler { |
| Object translate(String valueText) { |
| return valueText; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") // creating an instance with a non-enum type is an error! |
| static class EnumHandler extends Handler { |
| private final Class<?> enumType; |
| |
| public EnumHandler(Class<?> enumType) { |
| this.enumType = enumType; |
| } |
| |
| Object translate(String valueText) { |
| try { |
| return Enum.valueOf((Class) enumType, valueText.toUpperCase()); |
| } catch (IllegalArgumentException e) { |
| return null; |
| } |
| } |
| } |
| |
| static class FileHandler extends Handler { |
| Object translate(String valueText) { |
| return new File(valueText).getAbsoluteFile(); |
| } |
| } |
| } |