1 /* AbstractOptions.java Copyright 2000 Quowong P Liu
2 *
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 */
17
18 import java.io.OutputStreamWriter;
19 import java.io.PrintStream;
20 import java.io.PrintWriter;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Method;
23 import java.util.Arrays;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.LinkedList;
28 import java.util.List;
29
30 /**
31 * Abstract class for getting command line options with functionality
32 * similar to GNU getopt. One uses this class by extending it and
33 * adding fields. Then, when {@link #init(String[]) init} is called with
34 * the command line arguments, the fields that correspond to the command
35 * line options by being named appropriately are set according to the
36 * arguments. This works by using the java reflection mechanism.
37 * <p>
38 * The advantage of doing something like this over getopt-like functions
39 * is that all the code implementing a particular option, including the
40 * usage help, can be grouped together, separate from all other options.
41 * Adding an option can be as simple as adding a single field.
42 * <p>
43 * The functionality is intended to be similar to that of GNU getopt. Long
44 * and short options, optional and required arguments, and abbreviated long
45 * options are supported. Additionally, support for automatically generated
46 * usage help is provided.
47 * <p>
48 * Here is the section from the GNU <code>getopt</code>(1) man page
49 * describing how the command line arguments are interpreted:
50 * <blockquote>
51 * The parameters are parsed from left to right. Each parameter is
52 * classified as a short option, a long option, an argument to an option,
53 * or a non-option parameter.
54 * <p>
55 * A simple short option is a `-' followed by a short option character.
56 * If the option has a required argument, it may be written directly
57 * after the option character or as the next parameter (ie. separated by
58 * whitespace on the command line). If the option has an optional
59 * argument, it must be written directly after the option character if
60 * present.
61 * <p>
62 * It is possible to specify several short options after one `-' as long
63 * as all (except possibly the last) do not have required or optional
64 * arguments.
65 * <p>
66 * A long option normally begins with `--' followed by the long option
67 * name. If the option has a required argument, it may be written
68 * directly after the long option name, separated by `=', or as the next
69 * argument (ie. separated by whitespace on the command line). If the
70 * option has an optional argument, it must be written directly after the
71 * long option name, separated by `=', if present (if you add the `=' but
72 * nothing behind it, it is interpreted as if no argument was present;
73 * this is a slight bug, see the BUGS). Long options may be abbreviated,
74 * as long as the abbreviation is not ambiguous.
75 * <p>
76 * Each parameter not starting with a `-, and not a required argument of
77 * a previous option, is a non-option parameter. Each parameter after a
78 * `--' parameter is always interpreted as a non-option parameter. If
79 * the environment variable POSIXLY_CORRECT is set, or if the short
80 * option string started with a `+', all remaining parameters are
81 * interpreted as non-option parameters as soon as the first non-option
82 * parameter is found.
83 * </blockquote>
84 * <p>
85 * To create an option, there is one required field. It should be a
86 * <code>String</code>, <code>int</code> or <code>boolean</code> field
87 * with a name that ends with <code>_option</code>. In other words, the
88 * field should be named <i>opt</i><code>_option</code>, where
89 * <i>opt</i> is the name of the option. <code>String</code> and
90 * <code>int</code> options have required arguments, and will be set to
91 * those arguments if encountered. <code>boolean</code> options will be
92 * set to <code>true</code> if encountered, and, optionally, can take
93 * optional arguments.
94 * <p>
95 * The default long option name is <code>--</code> followed by the
96 * option name, <i>opt</i> with any underscores replaced with dashes.
97 * To override it, declare a <code>String</code> or <code>String[]</code>
98 * field named <i>opt</i><code>_longName</code> containing the long option
99 * name or names. All names must begin with <code>--</code>. If an array
100 * is used, all the names in it will be synonyms for this option.
101 * <p>
102 * By default, there is no equivalent short option. To provide short
103 * options, declare a <code>char</code> or <code>String</code> field
104 * named <i>opt</i><code>_shortName</code> containing the short option
105 * character or characters. If a string value is used, all the
106 * characters in the string will be synonyms for this option.
107 * <p>
108 * If no long option is desired, declare a <code>char</code> or
109 * <code>String</code> field named <i>opt</i><code>_shortOnly</code>.
110 * The meaning is the same as that of fields named
111 * <i>opt</i><code>_shortName</code>, except no long options are
112 * recognized for this option.
113 * <p>
114 * If a <code>boolean</code> option needs an optional argument, declare
115 * a <code>String</code> or <code>int</code> field named
116 * <i>opt</i><code>_optionalArg</code>. If the option is given an argument
117 * when encountered, this field will be set to it.
118 * <p>
119 * To provide an initial value to the option that needs to be determined
120 * at run-time, a declare method returning <code>void</code> that takes
121 * no arguments named <i>opt</i><code>_initDefault</code>. The method
122 * will be called before any of the arguments are processed. This
123 * initialization could be done in the constructor or somewhere else
124 * before {@link #init(String[]) init} is called, but putting it in a
125 * special method like this allows all the code implementing a particular
126 * option to be grouped together.
127 * <p>
128 * The default help usage for an option is a comma separated list
129 * of the short and long option names. If the option takes a required
130 * argument, each name is followed by the argument description. If the
131 * option takes an optional argument, each name is followed by the
132 * argument description surrounded by brackets. To override the help
133 * usage, declare a <code>String</code> field named
134 * <i>opt</i><code>_usage</code> containing the usage help.
135 * <p>
136 * The default argument description is the option name, <i>opt</i>, with
137 * all its letters capitalized. This is only used in the default help
138 * usage for options with required or optional arguments. To override
139 * it, declare a <code>String</code> field with the name
140 * <i>opt</i><code>_argDescription</code> containing the argument description.
141 * <p>
142 * The help usage also prints a description of the option if provided.
143 * By default, there is no description. To provide one, declare a
144 * <code>String</code> field named <i>opt</i><code>_description</code>
145 * containing the description.
146 * <p>
147 * After each time an option is successfully handled, if a method returning
148 * <code>void</code>, taking no arguments, and named
149 * <i>opt</i><code>_seen</code> exists, it is called.
150 * <p>
151 * Brief summary:
152 * <pre>
153 * String|int|boolean <i>opt</i>_option
154 * String|String[] <i>opt</i>_longName
155 * char|String <i>opt</i>_shortName
156 * char|String <i>opt</i>_shortOnly
157 * String|int <i>opt</i>_optionalArg
158 * void <i>opt</i>_initDefault()
159 * String <i>opt</i>_usage
160 * String <i>opt</i>_argDescription
161 * String <i>opt</i>_description
162 * void <i>opt</i>_seen()
163 * </pre>
164 *
165 * @author Quowong P Liu <qpliu@yahoo.com>
166 * @version $Id$
167 */
168 public abstract class AbstractOptions
169 {
170 private HashMap options = new HashMap();
171 private String[] longOptionNames;
172 private boolean parseSuccessful;
173
174 /**
175 * Set options based on the arguments provided.
176 * Calls {@link #init(String[],boolean,boolean,boolean) init}
177 * with <code>useLong</code> <code>true</code>, and
178 * <code>allLong</code> and <code>posixlyCorrect</code>
179 * <code>false</code>.
180 *
181 * @param arguments the command line argument
182 * @throws Exception only if child class is set up incorrectly
183 */
184 protected String[] init(String[] arguments)
185 throws Exception
186 {
187 return init(arguments, true, false, false);
188 }
189
190 /**
191 * Set options based on the arguments provided.
192 * Calls {@link #init(String[],boolean,boolean,boolean) init}
193 * with <code>allLong</code> and <code>posixlyCorrect</code>
194 * <code>false</code>.
195 *
196 * @param arguments the command line argument
197 * @param useLong recognize long options
198 * @throws Exception only if child class is set up incorrectly
199 */
200 protected String[] init(String[] arguments, boolean useLong)
201 throws Exception
202 {
203 return init(arguments, useLong, false, false);
204 }
205
206 /**
207 * Set options based on the arguments provided.
208 * Calls {@link #init(String[],boolean,boolean,boolean) init}
209 * with <code>posixlyCorrect</code> <code>false</code>.
210 *
211 * @param arguments the command line argument
212 * @param useLong recognize long options
213 * @param allLong try to treat all options as long
214 * @throws Exception only if child class is set up incorrectly
215 */
216 protected String[] init
217 (String[] arguments, boolean useLong, boolean allLong)
218 throws Exception
219 {
220 return init(arguments, useLong, allLong, false);
221 }
222
223 /**
224 * Set options based on the arguments provided.
225 * <p>
226 * Exceptions should only be thrown if the child is set up incorrectly.
227 * Reasons for throwing exceptions are
228 * <ul>
229 * <li>duplicate option names
230 * <li>{@link #fieldGet(Field) fieldGet},
231 * {@link #fieldSet(Field,Object) fieldSet}, or
232 * {@link #methodInvoke(Method,Object[]) methodInvoke}
233 * doesn't have the needed permissions
234 * <li>an option defines no names
235 * <li>a long option name doesn't start with <code>--</code>
236 * <li>an option field is declared with an invalid type,
237 * for example, if <i>opt</i><code>_longName</code> is
238 * neither a <code>String</code> nor a <code>String[]</code>.
239 * <li>an option method, <i>opt</i><code>_initDefault</code> or
240 * <i>opt</i><code>_seen</code>, throws an exception
241 * </ul>
242 *
243 * @param arguments the command line argument
244 * @param useLong recognize long options
245 * @param allLong try to treat all options as long
246 * @param posixlyCorrect treat all arguments following the first non-option as non-options
247 * @throws Exception only if child class is set up incorrectly
248 */
249 protected String[] init
250 (String[] arguments,
251 boolean useLong,
252 boolean allLong,
253 boolean posixlyCorrect)
254 throws Exception
255 {
256 initOptions();
257
258 List nonoptions = new LinkedList();
259 List args = new LinkedList();
260 for (int i = 0; i < arguments.length; i++)
261 args.add(arguments[i]);
262
263 parseSuccessful = true;
264 for (Iterator i = args.iterator(); i.hasNext(); ) {
265 String arg = (String) i.next();
266
267 if (arg.equals("--")) {
268 while (i.hasNext())
269 nonoptions.add(i.next());
270 break;
271 }
272
273 if (arg.equals("-") || !arg.startsWith("-")) {
274 nonoptions.add(arg);
275 if (posixlyCorrect) {
276 while (i.hasNext())
277 nonoptions.add(i.next());
278 break;
279 }
280 continue;
281 }
282
283 if (handleLongOption(i, arg, useLong, allLong))
284 continue;
285
286 handleShortOptions(i, arg);
287 }
288
289 String[] result = new String[nonoptions.size()];
290 return (String[]) nonoptions.toArray(result);
291 }
292
293 /**
294 * Whether the arguments were successfully parsed by
295 * {@link #init(String[],boolean,boolean,boolean) init}.
296 * Reasons for not successfully parsing the arguments
297 * are unrecognized options
298 * (see {@link #invalidOption(String) invalidOption}),
299 * missing required option arguments
300 * (see {@link #argumentMissing(String) argumentMissing}),
301 * ambiguous abbreviations of long options
302 * (see {@link #ambiguousOption(String) ambiguousOption}),
303 * and invalid numbers for options that take numeric arguments
304 * (see
305 * {@link #invalidOptionArgument(String,String) invalidOptionArgument}).
306 *
307 * @return whether arguments were successfully parsed
308 */
309 public boolean parseSuccessful()
310 {
311 return parseSuccessful;
312 }
313
314 /**
315 * Print usage help.
316 * Calls {@link #printUsage(PrintWriter) printUsage}.
317 *
318 * @param out where usage is printed
319 * @throws Exception only if child class is set up incorrectly
320 */
321 public void printUsage(PrintStream out)
322 throws Exception
323 {
324 PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
325 printUsage(pw);
326 pw.flush();
327 }
328
329 /**
330 * Print usage help.
331 * Default is to
332 * {@link #printUsage(Field,PrintWriter) printUsage} for
333 * each option.
334 *
335 * @param out where usage is printed
336 * @throws Exception only if child class is set up incorrectly
337 */
338 public void printUsage(PrintWriter out)
339 throws Exception
340 {
341 Field[] fields = getClass().getDeclaredFields();
342 for (int i = 0; i < fields.length; i++)
343 if (fields[i].getName().endsWith("_option"))
344 printUsage(fields[i], out);
345 out.flush();
346 }
347
348 /**
349 * Print usage help for a given option.
350 *
351 * @param option the option
352 * @param out where usage is printed
353 * @throws Exception only if child class is set up incorrectly
354 */
355 protected void printUsage(Field option, PrintWriter out)
356 throws Exception
357 {
358 String usage = getOptionUsage(option);
359 Field description = getOptionField(option, "description");
360 String spacing = " ";
361 String usagePadding = " ";
362
363 out.print(spacing);
364 if (description == null) {
365 out.println(usage);
366 return;
367 }
368
369 if (usage.length() > usagePadding.length()) {
370 out.println(usage);
371 out.print(spacing);
372 out.print(usagePadding);
373 out.print(spacing);
374 } else {
375 out.print(usage);
376 out.print(usagePadding.substring(usage.length()));
377 out.print(spacing);
378 }
379
380 int descWidth = 79 - 2*spacing.length() - usagePadding.length();
381 String desc = (String) fieldGet(description);
382 int start = 0;
383 while (start < desc.length()) {
384 if (start > 0) {
385 out.print(spacing);
386 out.print(usagePadding);
387 out.print(spacing);
388 }
389 if (desc.length() - start <= descWidth) {
390 out.println(desc.substring(start));
391 return;
392 }
393 int nextStart = start + descWidth;
394 if (nextStart >= desc.length())
395 nextStart = desc.length()-1;
396 int end = nextStart;
397 for (int i = nextStart; i > start; i--)
398 if (Character.isSpaceChar(desc.charAt(i))) {
399 end = i;
400 while (end > start
401 && Character.isSpaceChar(desc.charAt(end-1)))
402 end--;
403 nextStart = i;
404 while (nextStart < desc.length()
405 && Character.isSpaceChar(desc.charAt(nextStart)))
406 nextStart++;
407 break;
408 }
409 out.println(desc.substring(start, end));
410 start = nextStart;
411 }
412 }
413
414 private String getOptionUsage(Field option)
415 throws Exception
416 {
417 Field usage = getOptionField(option, "usage");
418 if (usage != null)
419 return (String) fieldGet(usage);
420
421 String argDesc;
422 Field argDescription = getOptionField(option, "argDescription");
423 if (argDescription != null)
424 argDesc = (String) fieldGet(argDescription);
425 else
426 argDesc = getOptionName(option).toUpperCase();
427 boolean noArg = false;
428 boolean argOptional = false;
429
430 if (option.getType() == boolean.class) {
431 if (getOptionField(option, "optionalArg") == null)
432 noArg = true;
433 else
434 argOptional = true;
435 }
436
437 StringBuffer sb = new StringBuffer();
438 for (Iterator i = getOptionSwitches(option).iterator(); i.hasNext(); )
439 {
440 Object opt = i.next();
441 if (opt instanceof Character) {
442 sb.append('-').append(opt);
443 if (argOptional)
444 sb.append('[').append(argDesc).append(']');
445 else if (!noArg)
446 sb.append(' ').append(argDesc);
447 } else {
448 sb.append(opt);
449 if (argOptional)
450 sb.append("[=").append(argDesc).append(']');
451 else if (!noArg)
452 sb.append(' ').append(argDesc);
453 }
454 if (i.hasNext())
455 sb.append(", ");
456 }
457 return sb.toString();
458 }
459
460 private void handleShortOptions(Iterator i, String arg)
461 throws Exception
462 {
463 for (int j = 1; j < arg.length(); j++) {
464 Field option = (Field) options.get(new Character(arg.charAt(j)));
465 if (option == null) {
466 parseSuccessful = false;
467 invalidOption(arg);
468 return;
469 }
470
471 if (option.getType() == boolean.class) {
472 Field optionalArg = getOptionField(option, "optionalArg");
473 if (optionalArg != null && j+1 < arg.length()) {
474 if (optionalArg.getType() == int.class)
475 try {
476 fieldSet
477 (optionalArg, new Integer(arg.substring(j+1)));
478 } catch (NumberFormatException e) {
479 parseSuccessful = false;
480 invalidOption(arg);
481 return;
482 }
483 else
484 fieldSet(optionalArg, arg.substring(j+1));
485 }
486 fieldSet(option, Boolean.TRUE);
487 Method seen = getOptionMethod(option, "seen", null);
488 if (seen != null)
489 methodInvoke(seen, null);
490 if (optionalArg != null)
491 return;
492 continue;
493 }
494
495 Object optionArg;
496 if (j+1 < arg.length()) {
497 optionArg = arg.substring(j+1);
498 } else if (i.hasNext()) {
499 optionArg = i.next();
500 } else {
501 parseSuccessful = false;
502 argumentMissing(arg);
503 return;
504 }
505
506 if (option.getType() == int.class)
507 try {
508 optionArg = new Integer((String) optionArg);
509 } catch (NumberFormatException e) {
510 parseSuccessful = false;
511 invalidOptionArgument(arg, (String) optionArg);
512 return;
513 }
514
515 fieldSet(option, optionArg);
516 Method seen = getOptionMethod(option, "seen", null);
517 if (seen != null)
518 methodInvoke(seen, null);
519 return;
520 }
521 }
522
523 private boolean handleLongOption
524 (Iterator i, String arg, boolean useLong, boolean allLong)
525 throws Exception
526 {
527 if (!useLong && !allLong)
528 return false;
529 boolean maybeShort = !arg.startsWith("--");
530 if (maybeShort && !allLong)
531 return false;
532
533 String longArg = arg;
534 if (maybeShort)
535 longArg = "-" + longArg;
536 if (longArg.indexOf('=') >= 0)
537 longArg = longArg.substring(0, longArg.indexOf('='));
538
539 int index = findLongOption(longArg);
540 if (!longOptionNames[index].startsWith(longArg)) {
541 if (maybeShort)
542 return false;
543 parseSuccessful = false;
544 invalidOption(arg);
545 return true;
546 }
547
548 if (index+1 < longOptionNames.length
549 && !longOptionNames[index].equals(longArg)
550 && longOptionNames[index+1].startsWith(longArg))
551 {
552 if (maybeShort)
553 return false;
554 parseSuccessful = false;
555 ambiguousOption(arg);
556 return true;
557 }
558
559 Field option = (Field) options.get(longOptionNames[index]);
560
561 if (option.getType() == boolean.class) {
562 Field optionalArg = getOptionField(option, "optionalArg");
563 if (optionalArg == null) {
564 if (arg.indexOf('=') >= 0) {
565 parseSuccessful = false;
566 invalidOption(arg);
567 return true;
568 }
569 } else if (arg.indexOf('=') >= 0) {
570 if (optionalArg.getType() == int.class)
571 try {
572 fieldSet
573 (optionalArg,
574 new Integer(arg.substring(arg.indexOf('=')+1)));
575 } catch (NumberFormatException e) {
576 parseSuccessful = false;
577 invalidOption(arg);
578 return true;
579 }
580 else
581 fieldSet(optionalArg, arg.substring(arg.indexOf('=')+1));
582 }
583 fieldSet(option, Boolean.TRUE);
584 Method seen = getOptionMethod(option, "seen", null);
585 if (seen != null)
586 methodInvoke(seen, null);
587 return true;
588 }
589
590 Object optionArg;
591 if (arg.indexOf('=') >= 0) {
592 optionArg = arg.substring(arg.indexOf('=')+1);
593 } else if (i.hasNext()) {
594 optionArg = i.next();
595 } else {
596 parseSuccessful = false;
597 argumentMissing(arg);
598 return true;
599 }
600
601 if (option.getType() == int.class)
602 try {
603 optionArg = new Integer((String) optionArg);
604 } catch (NumberFormatException e) {
605 parseSuccessful = false;
606 invalidOptionArgument(arg, (String) optionArg);
607 return true;
608 }
609
610 fieldSet(option, optionArg);
611 Method seen = getOptionMethod(option, "seen", null);
612 if (seen != null)
613 methodInvoke(seen, null);
614 return true;
615 }
616
617 private int findLongOption(String arg)
618 {
619 int lo = 0;
620 int hi = longOptionNames.length - 1;
621 for (;;) {
622 int mid = (hi + lo)/2;
623 int cmp = arg.compareTo(longOptionNames[mid]);
624 if (cmp == 0)
625 return mid;
626 if (cmp < 0) {
627 if (mid <= lo)
628 return lo;
629 hi = mid;
630 } else {
631 if (mid + 1 >= hi)
632 return hi;
633 lo = mid + 1;
634 }
635 }
636 }
637
638 /**
639 * Called by {@link #init(String[],boolean,boolean,boolean) init}
640 * when an unrecognized option is seen.
641 * Default is to print a message and the usage help to
642 * <code>System.err</code> and exit.
643 *
644 * @param option the option
645 * @throws Exception only if child class is set up incorrectly
646 */
647 protected void invalidOption(String option)
648 throws Exception
649 {
650 System.err.println("Unrecognized option: " + option);
651