• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 vogar;
18 
19 import com.google.common.collect.Lists;
20 import java.io.File;
21 import java.io.IOException;
22 import java.lang.reflect.Field;
23 import java.lang.reflect.ParameterizedType;
24 import java.lang.reflect.Type;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import vogar.util.Strings;
33 
34 /**
35  * Parses command line options.
36  *
37  * Strings in the passed-in String[] are parsed left-to-right. Each
38  * String is classified as a short option (such as "-v"), a long
39  * option (such as "--verbose"), an argument to an option (such as
40  * "out.txt" in "-f out.txt"), or a non-option positional argument.
41  *
42  * A simple short option is a "-" followed by a short option
43  * character. If the option requires an argument (which is true of any
44  * non-boolean option), it may be written as a separate parameter, but
45  * need not be. That is, "-f out.txt" and "-fout.txt" are both
46  * acceptable.
47  *
48  * It is possible to specify multiple short options after a single "-"
49  * as long as all (except possibly the last) do not require arguments.
50  *
51  * A long option begins with "--" followed by several characters. If
52  * the option requires an argument, it may be written directly after
53  * the option name, separated by "=", or as the next argument. (That
54  * is, "--file=out.txt" or "--file out.txt".)
55  *
56  * A boolean long option '--name' automatically gets a '--no-name'
57  * companion. Given an option "--flag", then, "--flag", "--no-flag",
58  * "--flag=true" and "--flag=false" are all valid, though neither
59  * "--flag true" nor "--flag false" are allowed (since "--flag" by
60  * itself is sufficient, the following "true" or "false" is
61  * interpreted separately). You can use "yes" and "no" as synonyms for
62  * "true" and "false".
63  *
64  * Each String not starting with a "-" and not a required argument of
65  * a previous option is a non-option positional argument, as are all
66  * successive Strings. Each String after a "--" is a non-option
67  * positional argument.
68  *
69  * Parsing of numeric fields such byte, short, int, long, float, and
70  * double fields is supported. This includes both unboxed and boxed
71  * versions (e.g. int vs Integer). If there is a problem parsing the
72  * argument to match the desired type, a runtime exception is thrown.
73  *
74  * File option fields are supported by simply wrapping the string
75  * argument in a File object without testing for the existance of the
76  * file.
77  *
78  * Parameterized Collection fields such as List<File> and Set<String>
79  * are supported as long as the parameter type is otherwise supported
80  * by the option parser. The collection field should be initialized
81  * with an appropriate collection instance.
82  *
83  * Enum types are supported. Input may be in either CONSTANT_CASE or
84  * lower_case.
85  *
86  * The fields corresponding to options are updated as their options
87  * are processed. Any remaining positional arguments are returned as a
88  * List<String>.
89  *
90  * Here's a simple example:
91  *
92  * // This doesn't need to be a separate class, if your application doesn't warrant it.
93  * // Non-@Option fields will be ignored.
94  * class Options {
95  *     @Option(names = { "-q", "--quiet" })
96  *     boolean quiet = false;
97  *
98  *     // Boolean options require a long name if it's to be possible to explicitly turn them off.
99  *     // Here the user can use --no-color.
100  *     @Option(names = { "--color" })
101  *     boolean color = true;
102  *
103  *     @Option(names = { "-m", "--mode" })
104  *     String mode = "standard; // Supply a default just by setting the field.
105  *
106  *     @Option(names = { "-p", "--port" })
107  *     int portNumber = 8888;
108  *
109  *     // There's no need to offer a short name for rarely-used options.
110  *     @Option(names = { "--timeout" })
111  *     double timeout = 1.0;
112  *
113  *     @Option(names = { "-o", "--output-file" })
114  *     File output;
115  *
116  *     // Multiple options are added to the collection.
117  *     // The collection field itself must be non-null.
118  *     @Option(names = { "-i", "--input-file" })
119  *     List<File> inputs = new ArrayList<File>();
120  *
121  * }
122  *
123  * class Main {
124  *     public static void main(String[] args) {
125  *         Options options = new Options();
126  *         List<String> inputFilenames = new OptionParser(options).parse(args);
127  *         for (String inputFilename : inputFilenames) {
128  *             if (!options.quiet) {
129  *                 ...
130  *             }
131  *             ...
132  *         }
133  *     }
134  * }
135  *
136  * See also:
137  *
138  *  the getopt(1) man page
139  *  Python's "optparse" module (http://docs.python.org/library/optparse.html)
140  *  the POSIX "Utility Syntax Guidelines" (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02)
141  *  the GNU "Standards for Command Line Interfaces" (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces)
142  */
143 public class OptionParser {
144     private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
145     static {
handlers.put(boolean.class, new BooleanHandler())146         handlers.put(boolean.class, new BooleanHandler());
handlers.put(Boolean.class, new BooleanHandler())147         handlers.put(Boolean.class, new BooleanHandler());
148 
handlers.put(byte.class, new ByteHandler())149         handlers.put(byte.class, new ByteHandler());
handlers.put(Byte.class, new ByteHandler())150         handlers.put(Byte.class, new ByteHandler());
handlers.put(short.class, new ShortHandler())151         handlers.put(short.class, new ShortHandler());
handlers.put(Short.class, new ShortHandler())152         handlers.put(Short.class, new ShortHandler());
handlers.put(int.class, new IntegerHandler())153         handlers.put(int.class, new IntegerHandler());
handlers.put(Integer.class, new IntegerHandler())154         handlers.put(Integer.class, new IntegerHandler());
handlers.put(long.class, new LongHandler())155         handlers.put(long.class, new LongHandler());
handlers.put(Long.class, new LongHandler())156         handlers.put(Long.class, new LongHandler());
157 
handlers.put(float.class, new FloatHandler())158         handlers.put(float.class, new FloatHandler());
handlers.put(Float.class, new FloatHandler())159         handlers.put(Float.class, new FloatHandler());
handlers.put(double.class, new DoubleHandler())160         handlers.put(double.class, new DoubleHandler());
handlers.put(Double.class, new DoubleHandler())161         handlers.put(Double.class, new DoubleHandler());
162 
handlers.put(String.class, new StringHandler())163         handlers.put(String.class, new StringHandler());
handlers.put(File.class, new FileHandler())164         handlers.put(File.class, new FileHandler());
165     }
getHandler(Type type)166     Handler getHandler(Type type) {
167         if (type instanceof ParameterizedType) {
168             ParameterizedType parameterizedType = (ParameterizedType) type;
169             Class rawClass = (Class<?>) parameterizedType.getRawType();
170             if (!Collection.class.isAssignableFrom(rawClass)) {
171                 throw new RuntimeException("cannot handle non-collection parameterized type " + type);
172             }
173             Type actualType = parameterizedType.getActualTypeArguments()[0];
174             if (!(actualType instanceof Class)) {
175                 throw new RuntimeException("cannot handle nested parameterized type " + type);
176             }
177             return getHandler(actualType);
178         }
179         if (type instanceof Class) {
180             Class<?> classType = (Class) type;
181             if (Collection.class.isAssignableFrom(classType)) {
182                 // could handle by just having a default of treating
183                 // contents as String but consciously decided this
184                 // should be an error
185                 throw new RuntimeException(
186                         "cannot handle non-parameterized collection " + type + ". " +
187                         "use a generic Collection to specify a desired element type");
188             }
189             if (classType.isEnum()) {
190                 return new EnumHandler(classType);
191             }
192             return handlers.get(classType);
193         }
194         throw new RuntimeException("cannot handle unknown field type " + type);
195     }
196 
197     private final Object optionSource;
198     private final HashMap<String, Field> optionMap;
199     private final Map<Field, Object> defaultOptionMap;
200 
201     /**
202      * Constructs a new OptionParser for setting the @Option fields of 'optionSource'.
203      */
OptionParser(Object optionSource)204     public OptionParser(Object optionSource) {
205         this.optionSource = optionSource;
206         this.optionMap = makeOptionMap();
207         this.defaultOptionMap = new HashMap<Field, Object>();
208     }
209 
readFile(File configFile)210     public static String[] readFile(File configFile) {
211         if (!configFile.exists()) {
212             return new String[0];
213         }
214 
215         List<String> configFileLines;
216         try {
217             configFileLines = Strings.readFileLines(configFile);
218         } catch (IOException e) {
219             throw new RuntimeException(e);
220         }
221 
222         List<String> argsList = Lists.newArrayList();
223         for (String rawLine : configFileLines) {
224             String line = rawLine.trim();
225 
226             // allow comments and blank lines
227             if (line.startsWith("#") || line.isEmpty()) {
228                 continue;
229             }
230             int space = line.indexOf(' ');
231             if (space == -1) {
232                 argsList.add(line);
233             } else {
234                 argsList.add(line.substring(0, space));
235                 argsList.add(line.substring(space + 1).trim());
236             }
237         }
238 
239         return argsList.toArray(new String[argsList.size()]);
240     }
241 
242     /**
243      * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource' provided to the constructor.
244      * Returns a list of the positional arguments left over after processing all options.
245      */
parse(String[] args)246     public List<String> parse(String[] args) {
247         return parseOptions(Arrays.asList(args).iterator());
248     }
249 
parseOptions(Iterator<String> args)250     private List<String> parseOptions(Iterator<String> args) {
251         final List<String> leftovers = new ArrayList<String>();
252 
253         // Scan 'args'.
254         while (args.hasNext()) {
255             final String arg = args.next();
256             if (arg.equals("--")) {
257                 // "--" marks the end of options and the beginning of positional arguments.
258                 break;
259             } else if (arg.startsWith("--")) {
260                 // A long option.
261                 parseLongOption(arg, args);
262             } else if (arg.startsWith("-")) {
263                 // A short option.
264                 parseGroupedShortOptions(arg, args);
265             } else {
266                 // The first non-option marks the end of options.
267                 leftovers.add(arg);
268                 break;
269             }
270         }
271 
272         // Package up the leftovers.
273         while (args.hasNext()) {
274             leftovers.add(args.next());
275         }
276         return leftovers;
277     }
278 
fieldForArg(String name)279     private Field fieldForArg(String name) {
280         final Field field = optionMap.get(name);
281         if (field == null) {
282             throw new RuntimeException("unrecognized option '" + name + "'");
283         }
284         return field;
285     }
286 
parseLongOption(String arg, Iterator<String> args)287     private void parseLongOption(String arg, Iterator<String> args) {
288         String name = arg.replaceFirst("^--no-", "--");
289         String value = null;
290 
291         // Support "--name=value" as well as "--name value".
292         final int equalsIndex = name.indexOf('=');
293         if (equalsIndex != -1) {
294             value = name.substring(equalsIndex + 1);
295             name = name.substring(0, equalsIndex);
296         }
297 
298         final Field field = fieldForArg(name);
299         final Handler handler = getHandler(field.getGenericType());
300         if (value == null) {
301             if (handler.isBoolean()) {
302                 value = arg.startsWith("--no-") ? "false" : "true";
303             } else {
304                 value = grabNextValue(args, name, field);
305             }
306         }
307         setValue(field, arg, handler, value);
308     }
309 
310     // Given boolean options a and b, and non-boolean option f, we want to allow:
311     // -ab
312     // -abf out.txt
313     // -abfout.txt
314     // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids it.)
parseGroupedShortOptions(String arg, Iterator<String> args)315     private void parseGroupedShortOptions(String arg, Iterator<String> args) {
316         for (int i = 1; i < arg.length(); ++i) {
317             final String name = "-" + arg.charAt(i);
318             final Field field = fieldForArg(name);
319             final Handler handler = getHandler(field.getGenericType());
320             String value;
321             if (handler.isBoolean()) {
322                 value = "true";
323             } else {
324                 // We need a value. If there's anything left, we take the rest of this "short option".
325                 if (i + 1 < arg.length()) {
326                     value = arg.substring(i + 1);
327                     i = arg.length() - 1;
328                 } else {
329                     value = grabNextValue(args, name, field);
330                 }
331             }
332             setValue(field, arg, handler, value);
333         }
334     }
335 
336     @SuppressWarnings("unchecked")
setValue(Field field, String arg, Handler handler, String valueText)337     private void setValue(Field field, String arg, Handler handler, String valueText) {
338 
339         Object value = handler.translate(valueText);
340         if (value == null) {
341             final String type = field.getType().getSimpleName().toLowerCase();
342             throw new RuntimeException("couldn't convert '" + valueText + "' to a " + type + " for option '" + arg + "'");
343         }
344         try {
345             field.setAccessible(true);
346             // record the original value of the field so it can be reset
347             if (!defaultOptionMap.containsKey(field)) {
348                 defaultOptionMap.put(field, field.get(optionSource));
349             }
350             if (Collection.class.isAssignableFrom(field.getType())) {
351                 Collection collection = (Collection) field.get(optionSource);
352                 collection.add(value);
353             } else {
354                 field.set(optionSource, value);
355             }
356         } catch (IllegalAccessException ex) {
357             throw new RuntimeException("internal error", ex);
358         }
359     }
360 
361     /**
362      * Resets optionSource's fields to their defaults
363      */
reset()364     public void reset() {
365         for (Map.Entry<Field, Object> entry : defaultOptionMap.entrySet()) {
366             try {
367                 entry.getKey().set(optionSource, entry.getValue());
368             } catch (IllegalAccessException e) {
369                 throw new RuntimeException(e);
370             }
371         }
372     }
373 
374     // Returns the next element of 'args' if there is one. Uses 'name' and 'field' to construct a helpful error message.
grabNextValue(Iterator<String> args, String name, Field field)375     private String grabNextValue(Iterator<String> args, String name, Field field) {
376         if (!args.hasNext()) {
377             final String type = field.getType().getSimpleName().toLowerCase();
378             throw new RuntimeException("option '" + name + "' requires a " + type + " argument");
379         }
380         return args.next();
381     }
382 
383     // Cache the available options and report any problems with the options themselves right away.
makeOptionMap()384     private HashMap<String, Field> makeOptionMap() {
385         final HashMap<String, Field> optionMap = new HashMap<String, Field>();
386         final Class<?> optionClass = optionSource.getClass();
387         for (Field field : optionClass.getDeclaredFields()) {
388             if (field.isAnnotationPresent(Option.class)) {
389                 final Option option = field.getAnnotation(Option.class);
390                 final String[] names = option.names();
391                 if (names.length == 0) {
392                     throw new RuntimeException("found an @Option with no name!");
393                 }
394                 for (String name : names) {
395                     if (optionMap.put(name, field) != null) {
396                         throw new RuntimeException("found multiple @Options sharing the name '" + name + "'");
397                     }
398                 }
399                 if (getHandler(field.getGenericType()) == null) {
400                     throw new RuntimeException("unsupported @Option field type '" + field.getType() + "'");
401                 }
402             }
403         }
404         return optionMap;
405     }
406 
407     static abstract class Handler {
408         // Only BooleanHandler should ever override this.
isBoolean()409         boolean isBoolean() {
410             return false;
411         }
412 
413         /**
414          * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
415          * Returns null on failure.
416          */
translate(String valueText)417         abstract Object translate(String valueText);
418     }
419 
420     static class BooleanHandler extends Handler {
isBoolean()421         @Override boolean isBoolean() {
422             return true;
423         }
424 
translate(String valueText)425         Object translate(String valueText) {
426             if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
427                 return Boolean.TRUE;
428             } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
429                 return Boolean.FALSE;
430             }
431             return null;
432         }
433     }
434 
435     static class ByteHandler extends Handler {
translate(String valueText)436         Object translate(String valueText) {
437             try {
438                 return Byte.parseByte(valueText);
439             } catch (NumberFormatException ex) {
440                 return null;
441             }
442         }
443     }
444 
445     static class ShortHandler extends Handler {
translate(String valueText)446         Object translate(String valueText) {
447             try {
448                 return Short.parseShort(valueText);
449             } catch (NumberFormatException ex) {
450                 return null;
451             }
452         }
453     }
454 
455     static class IntegerHandler extends Handler {
translate(String valueText)456         Object translate(String valueText) {
457             try {
458                 return Integer.parseInt(valueText);
459             } catch (NumberFormatException ex) {
460                 return null;
461             }
462         }
463     }
464 
465     static class LongHandler extends Handler {
translate(String valueText)466         Object translate(String valueText) {
467             try {
468                 return Long.parseLong(valueText);
469             } catch (NumberFormatException ex) {
470                 return null;
471             }
472         }
473     }
474 
475     static class FloatHandler extends Handler {
translate(String valueText)476         Object translate(String valueText) {
477             try {
478                 return Float.parseFloat(valueText);
479             } catch (NumberFormatException ex) {
480                 return null;
481             }
482         }
483     }
484 
485     static class DoubleHandler extends Handler {
translate(String valueText)486         Object translate(String valueText) {
487             try {
488                 return Double.parseDouble(valueText);
489             } catch (NumberFormatException ex) {
490                 return null;
491             }
492         }
493     }
494 
495     static class StringHandler extends Handler {
translate(String valueText)496         Object translate(String valueText) {
497             return valueText;
498         }
499     }
500 
501     @SuppressWarnings("unchecked") // creating an instance with a non-enum type is an error!
502     static class EnumHandler extends Handler {
503         private final Class<?> enumType;
504 
EnumHandler(Class<?> enumType)505         public EnumHandler(Class<?> enumType) {
506             this.enumType = enumType;
507         }
508 
translate(String valueText)509         Object translate(String valueText) {
510             try {
511                 return Enum.valueOf((Class) enumType, valueText.toUpperCase());
512             } catch (IllegalArgumentException e) {
513                 return null;
514             }
515         }
516     }
517 
518     static class FileHandler extends Handler {
translate(String valueText)519         Object translate(String valueText) {
520             return new File(valueText).getAbsoluteFile();
521         }
522     }
523 }
524