001package jargs.gnu;
002
003import java.text.NumberFormat;
004import java.text.ParseException;
005import java.util.Hashtable;
006import java.util.Vector;
007import java.util.Enumeration;
008import java.util.Locale;
009
010/**
011 * Largely GNU-compatible command-line options parser. Has short (-v) and
012 * long-form (--verbose) option support, and also allows options with
013 * associated values (-d 2, --debug 2, --debug=2). Option processing
014 * can be explicitly terminated by the argument '--'.
015 *
016 * @author Steve Purcell
017 * @version $Revision: 1.5 $
018 * @see jargs.examples.gnu.OptionTest
019 */
020public class CmdLineParser {
021
022    /**
023     * Base class for exceptions that may be thrown when options are parsed
024     */
025    public static abstract class OptionException extends Exception {
026        OptionException(String msg) { super(msg); }
027    }
028
029    /**
030     * Thrown when the parsed command-line contains an option that is not
031     * recognised. <code>getMessage()</code> returns
032     * an error string suitable for reporting the error to the user (in
033     * English).
034     */
035    public static class UnknownOptionException extends OptionException {
036        UnknownOptionException( String optionName ) {
037            super("unknown option '" + optionName + "'");
038            this.optionName = optionName;
039        }
040
041        /**
042         * @return the name of the option that was unknown (e.g. "-u")
043         */
044        public String getOptionName() { return this.optionName; }
045        private String optionName = null;
046    }
047
048    /**
049     * Thrown when an illegal or missing value is given by the user for
050     * an option that takes a value. <code>getMessage()</code> returns
051     * an error string suitable for reporting the error to the user (in
052     * English).
053     */
054    public static class IllegalOptionValueException extends OptionException {
055        public IllegalOptionValueException( Option opt, String value ) {
056            super("illegal value '" + value + "' for option -" +
057                  opt.shortForm() + "/--" + opt.longForm());
058            this.option = opt;
059            this.value = value;
060        }
061
062        /**
063         * @return the name of the option whose value was illegal (e.g. "-u")
064         */
065        public Option getOption() { return this.option; }
066
067        /**
068         * @return the illegal value
069         */
070        public String getValue() { return this.value; }
071        private Option option;
072        private String value;
073    }
074
075    /**
076     * Representation of a command-line option
077     */
078    public static abstract class Option {
079
080        protected Option( char shortForm, String longForm,
081                          boolean wantsValue ) {
082            if ( longForm == null )
083                throw new IllegalArgumentException("null arg forms not allowed");
084            this.shortForm = new String(new char[]{shortForm});
085            this.longForm = longForm;
086            this.wantsValue = wantsValue;
087        }
088
089        public String shortForm() { return this.shortForm; }
090
091        public String longForm() { return this.longForm; }
092
093        /**
094         * Tells whether or not this option wants a value
095         */
096        public boolean wantsValue() { return this.wantsValue; }
097
098        public final Object getValue( String arg, Locale locale )
099            throws IllegalOptionValueException {
100            if ( this.wantsValue ) {
101                if ( arg == null ) {
102                    throw new IllegalOptionValueException(this, "");
103                }
104                return this.parseValue(arg, locale);
105            }
106            else {
107                return Boolean.TRUE;
108            }
109        }
110
111        /**
112         * Override to extract and convert an option value passed on the
113         * command-line
114         */
115        protected Object parseValue( String arg, Locale locale )
116            throws IllegalOptionValueException {
117            return null;
118        }
119
120        private String shortForm = null;
121        private String longForm = null;
122        private boolean wantsValue = false;
123
124        public static class BooleanOption extends Option {
125            public BooleanOption( char shortForm, String longForm ) {
126                super(shortForm, longForm, false);
127            }
128        }
129
130        /**
131         * An option that expects an integer value
132         */
133        public static class IntegerOption extends Option {
134            public IntegerOption( char shortForm, String longForm ) {
135                super(shortForm, longForm, true);
136            }
137            protected Object parseValue( String arg, Locale locale )
138                throws IllegalOptionValueException {
139                try {
140                    return new Integer(arg);
141                }
142                catch (NumberFormatException e) {
143                    throw new IllegalOptionValueException(this, arg);
144                }
145            }
146        }
147
148        /**
149         * An option that expects a floating-point value
150         */
151        public static class DoubleOption extends Option {
152            public DoubleOption( char shortForm, String longForm ) {
153                super(shortForm, longForm, true);
154            }
155            protected Object parseValue( String arg, Locale locale )
156                throws IllegalOptionValueException {
157                try {
158                    NumberFormat format = NumberFormat.getNumberInstance(locale);
159                    Number num = (Number)format.parse(arg);
160                    return new Double(num.doubleValue());
161                }
162                catch (ParseException e) {
163                    throw new IllegalOptionValueException(this, arg);
164                }
165            }
166        }
167
168        /**
169         * An option that expects a string value
170         */
171        public static class StringOption extends Option {
172            public StringOption( char shortForm, String longForm ) {
173                super(shortForm, longForm, true);
174            }
175            protected Object parseValue( String arg, Locale locale ) {
176                return arg;
177            }
178        }
179    }
180
181    /**
182     * Add the specified Option to the list of accepted options
183     */
184    public final Option addOption( Option opt ) {
185        this.options.put("-" + opt.shortForm(), opt);
186        this.options.put("--" + opt.longForm(), opt);
187        return opt;
188    }
189
190    /**
191     * Convenience method for adding a string option.
192     * @return the new Option
193     */
194    public final Option addStringOption( char shortForm, String longForm ) {
195        Option opt = new Option.StringOption(shortForm, longForm);
196        addOption(opt);
197        return opt;
198    }
199
200    /**
201     * Convenience method for adding an integer option.
202     * @return the new Option
203     */
204    public final Option addIntegerOption( char shortForm, String longForm ) {
205        Option opt = new Option.IntegerOption(shortForm, longForm);
206        addOption(opt);
207        return opt;
208    }
209
210    /**
211     * Convenience method for adding a double option.
212     * @return the new Option
213     */
214    public final Option addDoubleOption( char shortForm, String longForm ) {
215        Option opt = new Option.DoubleOption(shortForm, longForm);
216        addOption(opt);
217        return opt;
218    }
219
220    /**
221     * Convenience method for adding a boolean option.
222     * @return the new Option
223     */
224    public final Option addBooleanOption( char shortForm, String longForm ) {
225        Option opt = new Option.BooleanOption(shortForm, longForm);
226        addOption(opt);
227        return opt;
228    }
229
230    /**
231     * @return the parsed value of the given Option, or null if the
232     * option was not set
233     */
234    public final Object getOptionValue( Option o ) {
235        return values.get(o.longForm());
236    }
237
238    /**
239     * @return the non-option arguments
240     */
241    public final String[] getRemainingArgs() {
242        return this.remainingArgs;
243    }
244
245    /**
246     * Extract the options and non-option arguments from the given
247     * list of command-line arguments. The default locale is used for
248     * parsing options whose values might be locale-specific.
249     */
250    public final void parse( String[] argv )
251        throws IllegalOptionValueException, UnknownOptionException {
252        parse(argv, Locale.getDefault());
253    }
254
255    /**
256     * Extract the options and non-option arguments from the given
257     * list of command-line arguments. The specified locale is used for
258     * parsing options whose values might be locale-specific.
259     */
260    public final void parse( String[] argv, Locale locale )
261        throws IllegalOptionValueException, UnknownOptionException {
262        Vector otherArgs = new Vector();
263        int position = 0;
264        this.values = new Hashtable(10);
265        while ( position < argv.length ) {
266            String curArg = argv[position];
267            if ( curArg.startsWith("-") ) {
268                if ( curArg.equals("--") ) { // end of options
269                    position += 1;
270                    break;
271                }
272                String valueArg = null;
273                if ( curArg.startsWith("--") ) { // handle --arg=value
274                    int equalsPos = curArg.indexOf("=");
275                    if ( equalsPos != -1 ) {
276                        valueArg = curArg.substring(equalsPos+1);
277                        curArg = curArg.substring(0,equalsPos);
278                    }
279                }
280                Option opt = (Option)this.options.get(curArg);
281                if ( opt == null ) {
282                    throw new UnknownOptionException(curArg);
283                }
284                Object value = null;
285                if ( opt.wantsValue() ) {
286                    if ( valueArg == null ) {
287                        position += 1;
288                        valueArg = null;
289                        if ( position < argv.length ) {
290                            valueArg = argv[position];
291                        }
292                    }
293                    value = opt.getValue(valueArg, locale);
294                }
295                else {
296                    value = opt.getValue(null, locale);
297                }
298                this.values.put(opt.longForm(), value);
299                position += 1;
300            }
301            else {
302                break;
303            }
304        }
305        for ( ; position < argv.length; ++position ) {
306            otherArgs.addElement(argv[position]);
307        }
308
309        this.remainingArgs = new String[otherArgs.size()];
310        int i = 0;
311        for (Enumeration e = otherArgs.elements(); e.hasMoreElements(); ++i) {
312            this.remainingArgs[i] = (String)e.nextElement();
313        }
314    }
315
316    private String[] remainingArgs = null;
317    private Hashtable options = new Hashtable(10);
318    private Hashtable values = new Hashtable(10);
319}