2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package dalvik.runner;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.ParameterizedType;
22 import java.lang.reflect.Type;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.List;
31 * Parses command line options.
33 * Strings in the passed-in String[] are parsed left-to-right. Each
34 * String is classified as a short option (such as "-v"), a long
35 * option (such as "--verbose"), an argument to an option (such as
36 * "out.txt" in "-f out.txt"), or a non-option positional argument.
38 * A simple short option is a "-" followed by a short option
39 * character. If the option requires an argument (which is true of any
40 * non-boolean option), it may be written as a separate parameter, but
41 * need not be. That is, "-f out.txt" and "-fout.txt" are both
44 * It is possible to specify multiple short options after a single "-"
45 * as long as all (except possibly the last) do not require arguments.
47 * A long option begins with "--" followed by several characters. If
48 * the option requires an argument, it may be written directly after
49 * the option name, separated by "=", or as the next argument. (That
50 * is, "--file=out.txt" or "--file out.txt".)
52 * A boolean long option '--name' automatically gets a '--no-name'
53 * companion. Given an option "--flag", then, "--flag", "--no-flag",
54 * "--flag=true" and "--flag=false" are all valid, though neither
55 * "--flag true" nor "--flag false" are allowed (since "--flag" by
56 * itself is sufficient, the following "true" or "false" is
57 * interpreted separately). You can use "yes" and "no" as synonyms for
60 * Each String not starting with a "-" and not a required argument of
61 * a previous option is a non-option positional argument, as are all
62 * successive Strings. Each String after a "--" is a non-option
63 * positional argument.
65 * Parsing of numeric fields such byte, short, int, long, float, and
66 * double fields is supported. This includes both unboxed and boxed
67 * versions (e.g. int vs Integer). If there is a problem parsing the
68 * argument to match the desired type, a runtime exception is thrown.
70 * File option fields are supported by simply wrapping the string
71 * argument in a File object without testing for the existance of the
74 * Parameterized Collection fields such as List<File> and Set<String>
75 * are supported as long as the parameter type is otherwise supported
76 * by the option parser. The collection field should be initialized
77 * with an appropriate collection instance.
79 * The fields corresponding to options are updated as their options
80 * are processed. Any remaining positional arguments are returned as a
83 * Here's a simple example:
85 * // This doesn't need to be a separate class, if your application doesn't warrant it.
86 * // Non-@Option fields will be ignored.
88 * @Option(names = { "-q", "--quiet" })
89 * boolean quiet = false;
91 * // Boolean options require a long name if it's to be possible to explicitly turn them off.
92 * // Here the user can use --no-color.
93 * @Option(names = { "--color" })
94 * boolean color = true;
96 * @Option(names = { "-m", "--mode" })
97 * String mode = "standard; // Supply a default just by setting the field.
99 * @Option(names = { "-p", "--port" })
100 * int portNumber = 8888;
102 * // There's no need to offer a short name for rarely-used options.
103 * @Option(names = { "--timeout" })
104 * double timeout = 1.0;
106 * @Option(names = { "-o", "--output-file" })
109 * // Multiple options are added to the collection.
110 * // The collection field itself must be non-null.
111 * @Option(names = { "-i", "--input-file" })
112 * List<File> inputs = new ArrayList<File>();
117 * public static void main(String[] args) {
118 * Options options = new Options();
119 * List<String> inputFilenames = new OptionParser(options).parse(args);
120 * for (String inputFilename : inputFilenames) {
121 * if (!options.quiet) {
131 * the getopt(1) man page
132 * Python's "optparse" module (http://docs.python.org/library/optparse.html)
133 * the POSIX "Utility Syntax Guidelines" (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02)
134 * the GNU "Standards for Command Line Interfaces" (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces)
136 public class OptionParser {
137 private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
139 handlers.put(boolean.class, new BooleanHandler());
140 handlers.put(Boolean.class, new BooleanHandler());
142 handlers.put(byte.class, new ByteHandler());
143 handlers.put(Byte.class, new ByteHandler());
144 handlers.put(short.class, new ShortHandler());
145 handlers.put(Short.class, new ShortHandler());
146 handlers.put(int.class, new IntegerHandler());
147 handlers.put(Integer.class, new IntegerHandler());
148 handlers.put(long.class, new LongHandler());
149 handlers.put(Long.class, new LongHandler());
151 handlers.put(float.class, new FloatHandler());
152 handlers.put(Float.class, new FloatHandler());
153 handlers.put(double.class, new DoubleHandler());
154 handlers.put(Double.class, new DoubleHandler());
156 handlers.put(String.class, new StringHandler());
157 handlers.put(File.class, new FileHandler());
159 Handler getHandler(Type type) {
160 if (type instanceof ParameterizedType) {
161 ParameterizedType parameterizedType = (ParameterizedType) type;
162 Class rawClass = (Class<?>) parameterizedType.getRawType();
163 if (!Collection.class.isAssignableFrom(rawClass)) {
164 throw new RuntimeException("cannot handle non-collection parameterized type " + type);
166 Type actualType = parameterizedType.getActualTypeArguments()[0];
167 if (!(actualType instanceof Class)) {
168 throw new RuntimeException("cannot handle nested parameterized type " + type);
170 return getHandler(actualType);
172 if (type instanceof Class) {
173 if (Collection.class.isAssignableFrom((Class) type)) {
174 // could handle by just having a default of treating
175 // contents as String but consciously decided this
176 // should be an error
177 throw new RuntimeException(
178 "cannot handle non-parameterized collection " + type + ". " +
179 "use a generic Collection to specify a desired element type");
181 return handlers.get((Class<?>) type);
183 throw new RuntimeException("cannot handle unknown field type " + type);
186 private final Object optionSource;
187 private final HashMap<String, Field> optionMap;
190 * Constructs a new OptionParser for setting the @Option fields of 'optionSource'.
192 public OptionParser(Object optionSource) {
193 this.optionSource = optionSource;
194 this.optionMap = makeOptionMap();
198 * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource' provided to the constructor.
199 * Returns a list of the positional arguments left over after processing all options.
201 public List<String> parse(String[] args) {
202 return parseOptions(Arrays.asList(args).iterator());
205 private List<String> parseOptions(Iterator<String> args) {
206 final List<String> leftovers = new ArrayList<String>();
209 while (args.hasNext()) {
210 final String arg = args.next();
211 if (arg.equals("--")) {
212 // "--" marks the end of options and the beginning of positional arguments.
214 } else if (arg.startsWith("--")) {
216 parseLongOption(arg, args);
217 } else if (arg.startsWith("-")) {
219 parseGroupedShortOptions(arg, args);
221 // The first non-option marks the end of options.
227 // Package up the leftovers.
228 while (args.hasNext()) {
229 leftovers.add(args.next());
234 private Field fieldForArg(String name) {
235 final Field field = optionMap.get(name);
237 throw new RuntimeException("unrecognized option '" + name + "'");
242 private void parseLongOption(String arg, Iterator<String> args) {
243 String name = arg.replaceFirst("^--no-", "--");
246 // Support "--name=value" as well as "--name value".
247 final int equalsIndex = name.indexOf('=');
248 if (equalsIndex != -1) {
249 value = name.substring(equalsIndex + 1);
250 name = name.substring(0, equalsIndex);
253 final Field field = fieldForArg(name);
254 final Handler handler = getHandler(field.getGenericType());
256 if (handler.isBoolean()) {
257 value = arg.startsWith("--no-") ? "false" : "true";
259 value = grabNextValue(args, name, field);
262 setValue(optionSource, field, arg, handler, value);
265 // Given boolean options a and b, and non-boolean option f, we want to allow:
269 // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids it.)
270 private void parseGroupedShortOptions(String arg, Iterator<String> args) {
271 for (int i = 1; i < arg.length(); ++i) {
272 final String name = "-" + arg.charAt(i);
273 final Field field = fieldForArg(name);
274 final Handler handler = getHandler(field.getGenericType());
276 if (handler.isBoolean()) {
279 // We need a value. If there's anything left, we take the rest of this "short option".
280 if (i + 1 < arg.length()) {
281 value = arg.substring(i + 1);
282 i = arg.length() - 1;
284 value = grabNextValue(args, name, field);
287 setValue(optionSource, field, arg, handler, value);
291 @SuppressWarnings("unchecked")
292 private static void setValue(Object object, Field field, String arg, Handler handler, String valueText) {
294 Object value = handler.translate(valueText);
296 final String type = field.getType().getSimpleName().toLowerCase();
297 throw new RuntimeException("couldn't convert '" + valueText + "' to a " + type + " for option '" + arg + "'");
300 field.setAccessible(true);
301 if (Collection.class.isAssignableFrom(field.getType())) {
302 Collection collection = (Collection) field.get(object);
303 collection.add(value);
305 field.set(object, value);
307 } catch (IllegalAccessException ex) {
308 throw new RuntimeException("internal error", ex);
312 // Returns the next element of 'args' if there is one. Uses 'name' and 'field' to construct a helpful error message.
313 private String grabNextValue(Iterator<String> args, String name, Field field) {
314 if (!args.hasNext()) {
315 final String type = field.getType().getSimpleName().toLowerCase();
316 throw new RuntimeException("option '" + name + "' requires a " + type + " argument");
321 // Cache the available options and report any problems with the options themselves right away.
322 private HashMap<String, Field> makeOptionMap() {
323 final HashMap<String, Field> optionMap = new HashMap<String, Field>();
324 final Class<?> optionClass = optionSource.getClass();
325 for (Field field : optionClass.getDeclaredFields()) {
326 if (field.isAnnotationPresent(Option.class)) {
327 final Option option = field.getAnnotation(Option.class);
328 final String[] names = option.names();
329 if (names.length == 0) {
330 throw new RuntimeException("found an @Option with no name!");
332 for (String name : names) {
333 if (optionMap.put(name, field) != null) {
334 throw new RuntimeException("found multiple @Options sharing the name '" + name + "'");
337 if (getHandler(field.getGenericType()) == null) {
338 throw new RuntimeException("unsupported @Option field type '" + field.getType() + "'");
345 static abstract class Handler {
346 // Only BooleanHandler should ever override this.
347 boolean isBoolean() {
352 * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
353 * Returns null on failure.
355 abstract Object translate(String valueText);
358 static class BooleanHandler extends Handler {
359 @Override boolean isBoolean() {
363 Object translate(String valueText) {
364 if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
366 } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
367 return Boolean.FALSE;
373 static class ByteHandler extends Handler {
374 Object translate(String valueText) {
376 return Byte.parseByte(valueText);
377 } catch (NumberFormatException ex) {
383 static class ShortHandler extends Handler {
384 Object translate(String valueText) {
386 return Short.parseShort(valueText);
387 } catch (NumberFormatException ex) {
393 static class IntegerHandler extends Handler {
394 Object translate(String valueText) {
396 return Integer.parseInt(valueText);
397 } catch (NumberFormatException ex) {
403 static class LongHandler extends Handler {
404 Object translate(String valueText) {
406 return Long.parseLong(valueText);
407 } catch (NumberFormatException ex) {
413 static class FloatHandler extends Handler {
414 Object translate(String valueText) {
416 return Float.parseFloat(valueText);
417 } catch (NumberFormatException ex) {
423 static class DoubleHandler extends Handler {
424 Object translate(String valueText) {
426 return Double.parseDouble(valueText);
427 } catch (NumberFormatException ex) {
433 static class StringHandler extends Handler {
434 Object translate(String valueText) {
439 static class FileHandler extends Handler {
440 Object translate(String valueText) {
441 return new File(valueText);