OSDN Git Service

Remove TestRunner.success field / Make postCompileTest return void
[android-x86/dalvik.git] / libcore / tools / runner / java / dalvik / runner / OptionParser.java
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
17 package dalvik.runner;
18
19 import java.io.File;
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;
29
30 /**
31  * Parses command line options.
32  *
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.
37  *
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
42  * acceptable.
43  *
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.
46  *
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".)
51  *
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
58  * "true" and "false".
59  *
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.
64  *
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.
69  *
70  * File option fields are supported by simply wrapping the string
71  * argument in a File object without testing for the existance of the
72  * file.
73  *
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.
78  *
79  * The fields corresponding to options are updated as their options
80  * are processed. Any remaining positional arguments are returned as a
81  * List<String>.
82  *
83  * Here's a simple example:
84  *
85  * // This doesn't need to be a separate class, if your application doesn't warrant it.
86  * // Non-@Option fields will be ignored.
87  * class Options {
88  *     @Option(names = { "-q", "--quiet" })
89  *     boolean quiet = false;
90  *
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;
95  *
96  *     @Option(names = { "-m", "--mode" })
97  *     String mode = "standard; // Supply a default just by setting the field.
98  *
99  *     @Option(names = { "-p", "--port" })
100  *     int portNumber = 8888;
101  *
102  *     // There's no need to offer a short name for rarely-used options.
103  *     @Option(names = { "--timeout" })
104  *     double timeout = 1.0;
105  *
106  *     @Option(names = { "-o", "--output-file" })
107  *     File output;
108  *
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>();
113  *
114  * }
115  *
116  * class Main {
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) {
122  *                 ...
123  *             }
124  *             ...
125  *         }
126  *     }
127  * }
128  *
129  * See also:
130  *
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)
135  */
136 public class OptionParser {
137     private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
138     static {
139         handlers.put(boolean.class, new BooleanHandler());
140         handlers.put(Boolean.class, new BooleanHandler());
141
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());
150
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());
155
156         handlers.put(String.class, new StringHandler());
157         handlers.put(File.class, new FileHandler());
158     }
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);
165             }
166             Type actualType = parameterizedType.getActualTypeArguments()[0];
167             if (!(actualType instanceof Class)) {
168                 throw new RuntimeException("cannot handle nested parameterized type " + type);
169             }
170             return getHandler(actualType);
171         }
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");
180             }
181             return handlers.get((Class<?>) type);
182         }
183         throw new RuntimeException("cannot handle unknown field type " + type);
184     }
185
186     private final Object optionSource;
187     private final HashMap<String, Field> optionMap;
188
189     /**
190      * Constructs a new OptionParser for setting the @Option fields of 'optionSource'.
191      */
192     public OptionParser(Object optionSource) {
193         this.optionSource = optionSource;
194         this.optionMap = makeOptionMap();
195     }
196
197     /**
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.
200      */
201     public List<String> parse(String[] args) {
202         return parseOptions(Arrays.asList(args).iterator());
203     }
204
205     private List<String> parseOptions(Iterator<String> args) {
206         final List<String> leftovers = new ArrayList<String>();
207
208         // Scan 'args'.
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.
213                 break;
214             } else if (arg.startsWith("--")) {
215                 // A long option.
216                 parseLongOption(arg, args);
217             } else if (arg.startsWith("-")) {
218                 // A short option.
219                 parseGroupedShortOptions(arg, args);
220             } else {
221                 // The first non-option marks the end of options.
222                 leftovers.add(arg);
223                 break;
224             }
225         }
226
227         // Package up the leftovers.
228         while (args.hasNext()) {
229             leftovers.add(args.next());
230         }
231         return leftovers;
232     }
233
234     private Field fieldForArg(String name) {
235         final Field field = optionMap.get(name);
236         if (field == null) {
237             throw new RuntimeException("unrecognized option '" + name + "'");
238         }
239         return field;
240     }
241
242     private void parseLongOption(String arg, Iterator<String> args) {
243         String name = arg.replaceFirst("^--no-", "--");
244         String value = null;
245
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);
251         }
252
253         final Field field = fieldForArg(name);
254         final Handler handler = getHandler(field.getGenericType());
255         if (value == null) {
256             if (handler.isBoolean()) {
257                 value = arg.startsWith("--no-") ? "false" : "true";
258             } else {
259                 value = grabNextValue(args, name, field);
260             }
261         }
262         setValue(optionSource, field, arg, handler, value);
263     }
264
265     // Given boolean options a and b, and non-boolean option f, we want to allow:
266     // -ab
267     // -abf out.txt
268     // -abfout.txt
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());
275             String value;
276             if (handler.isBoolean()) {
277                 value = "true";
278             } else {
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;
283                 } else {
284                     value = grabNextValue(args, name, field);
285                 }
286             }
287             setValue(optionSource, field, arg, handler, value);
288         }
289     }
290
291     @SuppressWarnings("unchecked")
292     private static void setValue(Object object, Field field, String arg, Handler handler, String valueText) {
293
294         Object value = handler.translate(valueText);
295         if (value == null) {
296             final String type = field.getType().getSimpleName().toLowerCase();
297             throw new RuntimeException("couldn't convert '" + valueText + "' to a " + type + " for option '" + arg + "'");
298         }
299         try {
300             field.setAccessible(true);
301             if (Collection.class.isAssignableFrom(field.getType())) {
302                 Collection collection = (Collection) field.get(object);
303                 collection.add(value);
304             } else {
305                 field.set(object, value);
306             }
307         } catch (IllegalAccessException ex) {
308             throw new RuntimeException("internal error", ex);
309         }
310     }
311
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");
317         }
318         return args.next();
319     }
320
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!");
331                 }
332                 for (String name : names) {
333                     if (optionMap.put(name, field) != null) {
334                         throw new RuntimeException("found multiple @Options sharing the name '" + name + "'");
335                     }
336                 }
337                 if (getHandler(field.getGenericType()) == null) {
338                     throw new RuntimeException("unsupported @Option field type '" + field.getType() + "'");
339                 }
340             }
341         }
342         return optionMap;
343     }
344
345     static abstract class Handler {
346         // Only BooleanHandler should ever override this.
347         boolean isBoolean() {
348             return false;
349         }
350
351         /**
352          * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
353          * Returns null on failure.
354          */
355         abstract Object translate(String valueText);
356     }
357
358     static class BooleanHandler extends Handler {
359         @Override boolean isBoolean() {
360             return true;
361         }
362
363         Object translate(String valueText) {
364             if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
365                 return Boolean.TRUE;
366             } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
367                 return Boolean.FALSE;
368             }
369             return null;
370         }
371     }
372
373     static class ByteHandler extends Handler {
374         Object translate(String valueText) {
375             try {
376                 return Byte.parseByte(valueText);
377             } catch (NumberFormatException ex) {
378                 return null;
379             }
380         }
381     }
382
383     static class ShortHandler extends Handler {
384         Object translate(String valueText) {
385             try {
386                 return Short.parseShort(valueText);
387             } catch (NumberFormatException ex) {
388                 return null;
389             }
390         }
391     }
392
393     static class IntegerHandler extends Handler {
394         Object translate(String valueText) {
395             try {
396                 return Integer.parseInt(valueText);
397             } catch (NumberFormatException ex) {
398                 return null;
399             }
400         }
401     }
402
403     static class LongHandler extends Handler {
404         Object translate(String valueText) {
405             try {
406                 return Long.parseLong(valueText);
407             } catch (NumberFormatException ex) {
408                 return null;
409             }
410         }
411     }
412
413     static class FloatHandler extends Handler {
414         Object translate(String valueText) {
415             try {
416                 return Float.parseFloat(valueText);
417             } catch (NumberFormatException ex) {
418                 return null;
419             }
420         }
421     }
422
423     static class DoubleHandler extends Handler {
424         Object translate(String valueText) {
425             try {
426                 return Double.parseDouble(valueText);
427             } catch (NumberFormatException ex) {
428                 return null;
429             }
430         }
431     }
432
433     static class StringHandler extends Handler {
434         Object translate(String valueText) {
435             return valueText;
436         }
437     }
438
439     static class FileHandler extends Handler {
440         Object translate(String valueText) {
441             return new File(valueText);
442         }
443     }
444 }