You're here: Snippet Directory » Java (241)
Language:

AbstractOptions

Language: English
Programming Language: Java
Published by: qpliu [not registered]
Last Update: 5/15/2006
Views: 1015


Description

Provide functionality similar to GNU getopt -- long and short options, required and optional arguments, abbreviations of long options -- as well as automatically generated usage help. It uses the java reflection mechanism to determine what the options are rather than getopt-like API.

Code

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