// --- BEGIN COPYRIGHT BLOCK --- // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // // (C) 2007 Red Hat, Inc. // All rights reserved. // --- END COPYRIGHT BLOCK --- /** * Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, * copy, modify and redistribute is granted, provided that this copyright * notice is retained and the author is given credit whenever appropriate. * * This software is distributed "as is", without any warranty, including * any implied warranty of merchantability or fitness for a particular * use. The author assumes no responsibility for, and shall not be liable * for, any special, indirect, or consequential damages, or any damages * whatsoever, arising out of or in connection with the use of this * software. */ import java.io.PrintStream; import java.io.IOException; import java.io.LineNumberReader; import java.io.File; import java.io.FileReader; import java.io.Reader; import java.util.Vector; import java.lang.reflect.Array; /** * ArgParser is used to parse the command line arguments for a java * application program. It provides a compact way to specify options and match * them against command line arguments, with support for * range checking, * multiple option names (aliases), * single word options, * multiple values associated with an option, * multiple option invocation, * generating help information, * custom argument parsing, and * reading arguments from a file. The * last feature is particularly useful and makes it * easy to create ad-hoc configuration files for an application. * *

Basic Example

* *

Here is a simple example in which an application has three * command line options: * -theta (followed by a floating point value), * -file (followed by a string value), and * -debug, which causes a boolean value to be set. * *

 *
 * static public void main (String[] args)
 *  {
 *    // create holder objects for storing results ...
 * 
 *    DoubleHolder theta = new DoubleHolder();
 *    StringHolder fileName = new StringHolder();
 *    BooleanHolder debug = new BooleanHolder();
 * 
 *    // create the parser and specify the allowed options ...
 * 
 *    ArgParser parser = new ArgParser("java argparser.SimpleExample");
 *    parser.addOption ("-theta %f #theta value (in degrees)", theta); 
 *    parser.addOption ("-file %s #name of the operating file", fileName);
 *    parser.addOption ("-debug %v #enables display of debugging info", debug);
 *
 *    // match the arguments ...
 * 
 *    parser.matchAllArgs (args);
 *
 *    // and print out the values
 *
 *    System.out.println ("theta=" + theta.value);
 *    System.out.println ("fileName=" + fileName.value);
 *    System.out.println ("debug=" + debug.value);
 *  }
 * 
*

A command line specifying all three options might look like this: *

 * java argparser.SimpleExample -theta 7.8 -debug -file /ai/lloyd/bar 
 * 
* *

The application creates an instance of ArgParser and then adds * descriptions of the allowed options using {@link #addOption addOption}. The * method {@link #matchAllArgs(String[]) matchAllArgs} is then used to match * these options against the command line arguments. Values associated with * each option are returned in the value field of special * ``holder'' classes (e.g., {@link argparser.DoubleHolder DoubleHolder}, * {@link argparser.StringHolder StringHolder}, etc.). * *

The first argument to {@link #addOption addOption} is a string that * specifies (1) the option's name, (2) a conversion code for its associated * value (e.g., %f for floating point, %s for a * string, %v for a boolean flag), and (3) an optional description * (following the # character) which is used for generating help * messages. The second argument is the holder object through which the value * is returned. This may be either a type-specific object (such as {@link * argparser.DoubleHolder DoubleHolder} or {@link argparser.StringHolder * StringHolder}), an array of the appropriate type, or * an instance of * java.util.Vector. * *

By default, arguments that don't match the specified options, are out of range, or are otherwise formatted incorrectly, * will cause matchAllArgs to print a message and exit the * program. Alternatively, an application can use {@link * #matchAllArgs(String[],int,int) matchAllArgs(args,idx,exitFlags)} to obtain * an array of unmatched arguments which can then be * processed separately * *

Range Specification

* * The values associated with options can also be given range specifications. A * range specification appears in curly braces immediately following the * conversion code. In the code fragment below, we show how to specify an * option -name that expects to be provided with one of three * string values (john, mary, or jane), * an option -index that expects to be supplied with a integer * value in the range 1 to 256, an option -size that expects to be * supplied with integer values of either 1, 2, 4, 8, or 16, and an option * -foo that expects to be supplied with floating point values in * the ranges -99 < foo <= -50, or 50 <= foo < 99. * *
 *    StringHolder name = new StringHolder();
 *    IntHolder index = new IntHolder();
 *    IntHolder size = new IntHolder();
 *    DoubleHolder foo = new DoubleHolder();
 * 
 *    parser.addOption ("-name %s {john,mary,jane}", name);
 *    parser.addOption ("-index %d {[1,256]}", index);
 *    parser.addOption ("-size %d {1,2,4,8,16}", size);
 *    parser.addOption ("-foo %f {(-99,-50],[50,99)}", foo);
 * 
* * If an argument value does not lie within a specified range, an error is * generated. * *

Multiple Option Names

* * An option may be given several names, or aliases, in the form of * a comma seperated list: * *
 *    parser.addOption ("-v,--verbose %v #print lots of info");
 *    parser.addOption ("-of,-outfile,-outputFile %s #output file");
 * 
* *

Single Word Options

* * Normally, options are assumed to be "multi-word", meaning * that any associated value must follow the option as a * separate argument string. For * example, *
 *    parser.addOption ("-file %s #file name");
 * 
* will cause the parser to look for two strings in the argument list * of the form *
 *    -file someFileName
 * 
* However, if there is no white space separting the option's name from * it's conversion code, then values associated with that * option will be assumed to be part of the same argument * string as the option itself. For example, *
 *    parser.addOption ("-file=%s #file name");
 * 
* will cause the parser to look for a single string in the argument * list of the form *
 *    -file=someFileName
 * 
* Such an option is called a "single word" option. * *

* In cases where an option has multiple names, then this single * word behavior is invoked if there is no white space between * the last indicated name and the conversion code. However, previous * names in the list will still be given multi-word behavior * if there is white space between the name and the * following comma. For example, *

 *    parser.addOption ("-nb=,-number ,-n%d #number of blocks");
 * 
* will cause the parser to look for one, two, and one word constructions * of the forms *
 *    -nb=N
 *    -number N
 *    -nN
 * 
* *

Multiple Option Values

* * If may be useful for an option to be followed by several values. * For instance, we might have an option -velocity * which should be followed by three numbers denoting * the x, y, and z components of a velocity vector. * We can require multiple values for an option * by placing a multiplier specification, * of the form XN, where N is an integer, * after the conversion code (or range specification, if present). * For example, * *
 *    double[] pos = new double[3];
 *
 *    addOption ("-position %fX3 #position of the object", pos);
 * 
* will cause the parser to look for *
 *    -position xx yy zz
 * 
* * in the argument list, where xx, yy, and * zz are numbers. The values are stored in the array * pos. * * Options requiring multiple values must use arrays to * return their values, and cannot be used in single word format. * *

Multiple Option Invocation

* * Normally, if an option appears twice in the command list, the * value associated with the second instance simply overwrites the * value associated with the first instance. * * However, the application can instead arrange for the storage of all * values associated with multiple option invocation, by supplying a instance * of java.util.Vector to serve as the value holder. Then every * time the option appears in the argument list, the parser will create a value * holder of appropriate type, set it to the current value, and store the * holder in the vector. For example, the construction * *
 *    Vector vec = new Vector(10);
 *
 *    parser.addOption ("-foo %f", vec);
 *    parser.matchAllArgs(args);
 * 
* when supplied with an argument list that contains *
 *    -foo 1.2 -foo 1000 -foo -78
 * 
* * will create three instances of {@link argparser.DoubleHolder DoubleHolder}, * initialized to 1.2, 1000, and -78, * and store them in vec. * *

Generating help information

* * ArgParser automatically generates help information for the options, and this * information may be printed in response to a help option, or may be * queried by the application using {@link #getHelpMessage getHelpMessage}. * The information for each option consists of the option's name(s), it's * required value(s), and an application-supplied description. Value * information is generated automaticlly from the conversion code, range, and * multiplier specifications (although this can be overriden, as * described below). * The application-supplied description is whatever * appears in the specification string after the optional # * character. The string returned by {@link #getHelpMessage getHelpMessage} for * the first example above would be * *
 * Usage: java argparser.SimpleExample
 * Options include:
 * 
 * -help,-?                displays help information
 * -theta <float>          theta value (in degrees)
 * -file <string>          name of the operating file
 * -debug                  enables display of debugging info
 * 
* * The options -help and -? are including in the * parser by default as help options, and they automatically cause the help * message to be printed. To exclude these * options, one should use the constructor {@link #ArgParser(String,boolean) * ArgParser(synopsis,false)}. * Help options can also be specified by the application using {@link * #addOption addOption} and the conversion code %h. Help options * can be disabled using {@link #setHelpOptionsEnabled * setHelpOptionsEnabled(false)}. * *

* A description of the required values for an option can be * specified explicitly * by placing a second # character in the specification * string. Everything between the first and second # * characters then becomes the value description, and everything * after the second # character becomes the option * description. * For example, if the -theta option * above was specified with *

 *    parser.addOption ("-theta %f #NUMBER#theta value (in degrees)",theta);
 * 
* instead of *
 *    parser.addOption ("-theta %f #theta value (in degrees)", theta);
 * 
* then the corresponding entry in the help message would look * like *
 * -theta NUMBER          theta value (in degrees)
 * 
* *

Custom Argument Parsing

* * An application may find it necessary to handle arguments that * don't fit into the framework of this class. There are a couple * of ways to do this. * *

* First, the method {@link #matchAllArgs(String[],int,int) * matchAllArgs(args,idx,exitFlags)} returns an array of * all unmatched arguments, which can then be handled * specially: *

 *    String[] unmatched =
 *       parser.matchAllArgs (args, 0, parser.EXIT_ON_ERROR);
 *    for (int i = 0; i < unmatched.length; i++)
 *     { ... handle unmatched arguments ...
 *     }
 * 
* * For instance, this would be useful for an applicatoon that accepts an * arbitrary number of input file names. The options can be parsed using * matchAllArgs, and the remaining unmatched arguments * give the file names. * *

If we need more control over the parsing, we can parse arguments one at * a time using {@link #matchArg matchArg}: * *

 *    int idx = 0;
 *    while (idx < args.length)
 *     { try
 *        { idx = parser.matchArg (args, idx);
 *          if (parser.getUnmatchedArgument() != null)
 *           {
 *             ... handle this unmatched argument ourselves ...
 *           }
 *        }
 *       catch (ArgParserException e) 
 *        { // malformed or erroneous argument
 *          parser.printErrorAndExit (e.getMessage());
 *        }
 *     }
 * 
* * {@link #matchArg matchArg(args,idx)} matches one option at location * idx in the argument list, and then returns the location value * that should be used for the next match. If an argument does * not match any option, * {@link #getUnmatchedArgument getUnmatchedArgument} will return a copy of the * unmatched argument. * *

Reading Arguments From a File

* * The method {@link #prependArgs prependArgs} can be used to automatically * read in a set of arguments from a file and prepend them onto an existing * argument list. Argument words correspond to white-space-delimited strings, * and the file may contain the comment character # (which * comments out everything to the end of the current line). A typical usage * looks like this: * *
 *    ... create parser and add options ...
 * 
 *    args = parser.prependArgs (new File(".configFile"), args);
 *
 *    parser.matchAllArgs (args);
 * 
* * This makes it easy to generate simple configuration files for an * application. * * @author John E. Lloyd, Fall 2004 */ public class ArgParser { Vector matchList; // int tabSpacing = 8; String synopsisString; boolean helpOptionsEnabled = true; Record defaultHelpOption = null; Record firstHelpOption = null; PrintStream printStream = System.out; int helpIndent = 24; String errMsg = null; String unmatchedArg = null; static String validConversionCodes = "iodxcbfsvh"; /** * Indicates that the program should exit with an appropriate message * in the event of an erroneous or malformed argument.*/ public static int EXIT_ON_ERROR = 1; /** * Indicates that the program should exit with an appropriate message * in the event of an unmatched argument.*/ public static int EXIT_ON_UNMATCHED = 2; /** * Returns a string containing the valid conversion codes. These * are the characters which may follow the % character in * the specification string of {@link #addOption addOption}. * * @return Valid conversion codes * @see #addOption */ public static String getValidConversionCodes() { return validConversionCodes; } static class NameDesc { String name; // oneWord implies that any value associated with // option is concatenated onto the argument string itself boolean oneWord; NameDesc next = null; } static class RangePnt { double dval = 0; long lval = 0; String sval = null; boolean bval = true; boolean closed = true; RangePnt (String s, boolean closed) { sval = s; this.closed = closed; } RangePnt (double d, boolean closed) { dval = d; this.closed = closed; } RangePnt (long l, boolean closed) { lval = l; this.closed = closed; } RangePnt (boolean b, boolean closed) { bval = b; this.closed = closed; } RangePnt (StringScanner scanner, int type) throws IllegalArgumentException { String typeName = null; try { switch (type) { case Record.CHAR: { typeName = "character"; lval = scanner.scanChar(); break; } case Record.INT: case Record.LONG: { typeName = "integer"; lval = scanner.scanInt(); break; } case Record.FLOAT: case Record.DOUBLE: { typeName = "float"; dval = scanner.scanDouble(); break; } case Record.STRING: { typeName = "string"; sval = scanner.scanString(); break; } case Record.BOOLEAN: { typeName = "boolean"; bval = scanner.scanBoolean(); break; } } } catch (StringScanException e) { throw new IllegalArgumentException ( "Malformed " + typeName + " '" + scanner.substring(scanner.getIndex(), e.getFailIndex()+1) + "' in range spec"); } // this.closed = closed; } void setClosed (boolean closed) { this.closed = closed; } boolean getClosed() { return closed; } int compareTo (double d) { if (dval < d) { return -1; } else if (d == dval) { return 0; } else { return 1; } } int compareTo (long l) { if (lval < l) { return -1; } else if (l == lval) { return 0; } else { return 1; } } int compareTo (String s) { return sval.compareTo (s); } int compareTo (boolean b) { if (b == bval) { return 0; } else { return 1; } } public String toString() { return "{ dval=" + dval + ", lval=" + lval + ", sval=" + sval + ", bval=" + bval + ", closed=" + closed + "}"; } } class RangeAtom { RangePnt low = null; RangePnt high = null; RangeAtom next = null; RangeAtom (RangePnt p0, RangePnt p1, int type) throws IllegalArgumentException { int cmp = 0; switch (type) { case Record.CHAR: case Record.INT: case Record.LONG: { cmp = p0.compareTo (p1.lval); break; } case Record.FLOAT: case Record.DOUBLE: { cmp = p0.compareTo (p1.dval); break; } case Record.STRING: { cmp = p0.compareTo (p1.sval); break; } } if (cmp > 0) { // then switch high and low low = p1; high = p0; } else { low = p0; high = p1; } } RangeAtom (RangePnt p0) throws IllegalArgumentException { low = p0; } boolean match (double d) { int lc = low.compareTo(d); if (high != null) { int hc = high.compareTo(d); return (lc*hc < 0 || (low.closed && lc==0) || (high.closed && hc==0)); } else { return lc == 0; } } boolean match (long l) { int lc = low.compareTo(l); if (high != null) { int hc = high.compareTo(l); return (lc*hc < 0 || (low.closed && lc==0) || (high.closed && hc==0)); } else { return lc == 0; } } boolean match (String s) { int lc = low.compareTo(s); if (high != null) { int hc = high.compareTo(s); return (lc*hc < 0 || (low.closed && lc==0) || (high.closed && hc==0)); } else { return lc == 0; } } boolean match (boolean b) { return low.compareTo(b) == 0; } public String toString() { return "low=" + (low==null ? "null" : low.toString()) + ", high=" + (high==null ? "null" : high.toString()); } } class Record { NameDesc nameList; static final int NOTYPE = 0; static final int BOOLEAN = 1; static final int CHAR = 2; static final int INT = 3; static final int LONG = 4; static final int FLOAT = 5; static final int DOUBLE = 6; static final int STRING = 7; int type; int numValues; boolean vectorResult = false; boolean required = true; String helpMsg = null; String valueDesc = null; String rangeDesc = null; Object resHolder = null; RangeAtom rangeList = null; RangeAtom rangeTail = null; char convertCode; boolean vval = true; // default value for now NameDesc firstNameDesc() { return nameList; } RangeAtom firstRangeAtom() { return rangeList; } int numRangeAtoms() { int cnt = 0; for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) { cnt++; } return cnt; } void addRangeAtom (RangeAtom ra) { if (rangeList == null) { rangeList = ra; } else { rangeTail.next = ra; } rangeTail = ra; } boolean withinRange (double d) { if (rangeList == null) { return true; } for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) { if (ra.match (d)) { return true; } } return false; } boolean withinRange (long l) { if (rangeList == null) { return true; } for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) { if (ra.match (l)) { return true; } } return false; } boolean withinRange (String s) { if (rangeList == null) { return true; } for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) { if (ra.match (s)) { return true; } } return false; } boolean withinRange (boolean b) { if (rangeList == null) { return true; } for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) { if (ra.match (b)) { return true; } } return false; } String valTypeName() { switch (convertCode) { case 'i': { return ("integer"); } case 'o': { return ("octal integer"); } case 'd': { return ("decimal integer"); } case 'x': { return ("hex integer"); } case 'c': { return ("char"); } case 'b': { return ("boolean"); } case 'f': { return ("float"); } case 's': { return ("string"); } } return ("unknown"); } void scanValue (Object result, String name, String s, int resultIdx) throws ArgParseException { double dval = 0; String sval = null; long lval = 0; boolean bval = false; if (s.length()==0) { throw new ArgParseException (name, "requires a contiguous value"); } StringScanner scanner = new StringScanner(s); try { switch (convertCode) { case 'i': { lval = scanner.scanInt(); break; } case 'o': { lval = scanner.scanInt (8, false); break; } case 'd': { lval = scanner.scanInt (10, false); break; } case 'x': { lval = scanner.scanInt (16, false); break; } case 'c': { lval = scanner.scanChar(); break; } case 'b': { bval = scanner.scanBoolean(); break; } case 'f': { dval = scanner.scanDouble(); break; } case 's': { sval = scanner.getString(); break; } } } catch (StringScanException e) { throw new ArgParseException ( name, "malformed " + valTypeName() + " '" + s + "'"); } scanner.skipWhiteSpace(); if (!scanner.atEnd()) { throw new ArgParseException ( name, "malformed " + valTypeName() + " '" + s + "'"); } boolean outOfRange = false; switch (type) { case CHAR: case INT: case LONG: { outOfRange = !withinRange (lval); break; } case FLOAT: case DOUBLE: { outOfRange = !withinRange (dval); break; } case STRING: { outOfRange = !withinRange (sval); break; } case BOOLEAN: { outOfRange = !withinRange (bval); break; } } if (outOfRange) { String errmsg = "value " + s + " not in range "; throw new ArgParseException ( name, "value '" + s + "' not in range " + rangeDesc); } if (result.getClass().isArray()) { switch (type) { case BOOLEAN: { ((boolean[])result)[resultIdx] = bval; break; } case CHAR: { ((char[])result)[resultIdx] = (char)lval; break; } case INT: { ((int[])result)[resultIdx] = (int)lval; break; } case LONG: { ((long[])result)[resultIdx] = lval; break; } case FLOAT: { ((float[])result)[resultIdx] = (float)dval; break; } case DOUBLE: { ((double[])result)[resultIdx] = dval; break; } case STRING: { ((String[])result)[resultIdx] = sval; break; } } } else { switch (type) { case BOOLEAN: { ((BooleanHolder)result).value = bval; break; } case CHAR: { ((CharHolder)result).value = (char)lval; break; } case INT: { ((IntHolder)result).value = (int)lval; break; } case LONG: { ((LongHolder)result).value = lval; break; } case FLOAT: { ((FloatHolder)result).value = (float)dval; break; } case DOUBLE: { ((DoubleHolder)result).value = dval; break; } case STRING: { ((StringHolder)result).value = sval; break; } } } } } private String firstHelpOptionName() { if (firstHelpOption != null) { return firstHelpOption.nameList.name; } else { return null; } } /** * Creates an ArgParser with a synopsis * string, and the default help options -help and * -?. * * @param synopsisString string that briefly describes program usage, * for use by {@link #getHelpMessage getHelpMessage}. * @see ArgParser#getSynopsisString * @see ArgParser#getHelpMessage */ public ArgParser(String synopsisString) { this (synopsisString, true); } /** * Creates an ArgParser with a synopsis * string. The help options -help and * -? are added if defaultHelp * is true. * * @param synopsisString string that briefly describes program usage, * for use by {@link #getHelpMessage getHelpMessage}. * @param defaultHelp if true, adds the default help options * @see ArgParser#getSynopsisString * @see ArgParser#getHelpMessage */ public ArgParser(String synopsisString, boolean defaultHelp) { matchList = new Vector(128); this.synopsisString = synopsisString; if (defaultHelp) { addOption ("-help,-? %h #displays help information", null); defaultHelpOption = firstHelpOption = (Record)matchList.get(0); } } /** * Returns the synopsis string used by the parser. * The synopsis string is a short description of how to invoke * the program, and usually looks something like *

* * "java somepackage.SomeClass [options] files ..." * * *

It is used in help and error messages. * * @return synopsis string * @see ArgParser#setSynopsisString * @see ArgParser#getHelpMessage */ public String getSynopsisString () { return synopsisString; } /** * Sets the synopsis string used by the parser. * * @param s new synopsis string * @see ArgParser#getSynopsisString * @see ArgParser#getHelpMessage */ public void setSynopsisString (String s) { synopsisString = s; } /** * Indicates whether or not help options are enabled. * * @return true if help options are enabled * @see ArgParser#setHelpOptionsEnabled * @see ArgParser#addOption */ public boolean getHelpOptionsEnabled () { return helpOptionsEnabled; } /** * Enables or disables help options. Help options are those * associated with a conversion code of %h. If * help options are enabled, and a help option is matched, * then the string produced by * {@link #getHelpMessage getHelpMessage} * is printed to the default print stream and the program * exits with code 0. Otherwise, arguments which match help * options are ignored. * * @param enable enables help options if true. * @see ArgParser#getHelpOptionsEnabled * @see ArgParser#addOption * @see ArgParser#setDefaultPrintStream */ public void setHelpOptionsEnabled(boolean enable) { helpOptionsEnabled = enable; } /** * Returns the default print stream used for outputting help * and error information. * * @return default print stream * @see ArgParser#setDefaultPrintStream */ public PrintStream getDefaultPrintStream() { return printStream; } /** * Sets the default print stream used for outputting help * and error information. * * @param stream new default print stream * @see ArgParser#getDefaultPrintStream */ public void setDefaultPrintStream (PrintStream stream) { printStream = stream; } /** * Gets the indentation used by {@link #getHelpMessage * getHelpMessage}. * * @return number of indentation columns * @see ArgParser#setHelpIndentation * @see ArgParser#getHelpMessage */ public int getHelpIndentation() { return helpIndent; } /** * Sets the indentation used by {@link #getHelpMessage * getHelpMessage}. This is the number of columns that an option's help * information is indented. If the option's name and value information * can fit within this number of columns, then all information about * the option is placed on one line. Otherwise, the indented help * information is placed on a separate line. * * @param indent number of indentation columns * @see ArgParser#getHelpIndentation * @see ArgParser#getHelpMessage */ public void setHelpIndentation (int indent) { helpIndent = indent; } // public void setTabSpacing (int n) // { tabSpacing = n; // } // public int getTabSpacing () // { return tabSpacing; // } private void scanRangeSpec (Record rec, String s) throws IllegalArgumentException { StringScanner scanner = new StringScanner (s); int i0, i = 1; char c, c0, c1; scanner.setStringDelimiters (")],}"); c = scanner.getc(); // swallow the first '{' scanner.skipWhiteSpace(); while ((c=scanner.peekc()) != '}') { RangePnt p0, p1; if (c == '[' || c == '(') { if (rec.convertCode == 'v' || rec.convertCode == 'b') { throw new IllegalArgumentException ("Sub ranges not supported for %b or %v"); } c0 = scanner.getc(); // record & swallow character scanner.skipWhiteSpace(); p0 = new RangePnt (scanner, rec.type); scanner.skipWhiteSpace(); if (scanner.getc() != ',') { throw new IllegalArgumentException ("Missing ',' in subrange specification"); } p1 = new RangePnt (scanner, rec.type); scanner.skipWhiteSpace(); if ((c1=scanner.getc()) != ']' && c1 != ')') { throw new IllegalArgumentException ("Unterminated subrange"); } if (c0 == '(') { p0.setClosed (false); } if (c1 == ')') { p1.setClosed (false); } rec.addRangeAtom (new RangeAtom (p0, p1, rec.type)); } else { scanner.skipWhiteSpace(); p0 = new RangePnt (scanner, rec.type); rec.addRangeAtom (new RangeAtom (p0)); } scanner.skipWhiteSpace(); if ((c=scanner.peekc()) == ',') { scanner.getc(); scanner.skipWhiteSpace(); } else if (c != '}') { throw new IllegalArgumentException ("Range spec: ',' or '}' expected"); } } if (rec.numRangeAtoms()==1) { rec.rangeDesc = s.substring (1, s.length()-1); } else { rec.rangeDesc = s; } } private int defaultResultType (char convertCode) { switch (convertCode) { case 'i': case 'o': case 'd': case 'x': { return Record.LONG; } case 'c': { return Record.CHAR; } case 'v': case 'b': { return Record.BOOLEAN; } case 'f': { return Record.DOUBLE; } case 's': { return Record.STRING; } } return Record.NOTYPE; } /** * Adds a new option description to the parser. The method takes two * arguments: a specification string, and a result holder in which to * store the associated value. * *

The specification string has the general form * *

optionNames * %conversionCode * [{rangeSpec}] * [Xmultiplier] * [#valueDescription] * [#optionDescription] * *

* where *

* *

The result holder must be an object capable of holding * a value compatible with the conversion code, * or it must be a java.util.Vector. * When the option is matched, its associated value is * placed in the result holder. If the same option is * matched repeatedly, the result holder value will be overwritten, * unless the result holder is a java.util.Vector, * in which * case new holder objects for each match will be allocated * and added to the vector. Thus if * multiple instances of an option are desired by the * program, the result holder should be a * java.util.Vector. * *

If the result holder is not a Vector, then * it must correspond as follows to the conversion code: * * * * * * * * * * * * * * * * * * * * * * * * * * *
%i, %d, %x, * %o{@link argparser.IntHolder IntHolder}, * {@link argparser.LongHolder LongHolder}, int[], or * long[]
%f{@link argparser.FloatHolder FloatHolder}, * {@link argparser.DoubleHolder DoubleHolder}, * float[], or * double[]
%b, %v{@link argparser.BooleanHolder BooleanHolder} or * boolean[]
%s{@link argparser.StringHolder StringHolder} or * String[]
%c{@link argparser.CharHolder CharHolder} or * char[]
* *

In addition, if the multiplier is greater than 1, * then only the array type indicated above may be used, * and the array must be at least as long as the multiplier. * *

If the result holder is a * Vector, then the system will create an appropriate * result holder object and add it to the vector. Multiple occurances * of the option will cause multiple results to be added to the vector. * *

The object allocated by the system to store the result * will correspond to the conversion code as follows: * * * * * * * * * * * * * * * * * * * * * * * * * * *
%i, %d, %x, * %o{@link argparser.LongHolder LongHolder}, or * long[] if the multiplier value exceeds 1
%f{@link argparser.DoubleHolder DoubleHolder}, or * double[] if the multiplier value exceeds 1
%b, %v{@link argparser.BooleanHolder BooleanHolder}, or * boolean[] * if the multiplier value exceeds 1
%s{@link argparser.StringHolder StringHolder}, or * String[] * if the multiplier value exceeds 1
%c{@link argparser.CharHolder CharHolder}, or char[] * if the multiplier value exceeds 1
* * @param spec the specification string * @param resHolder object in which to store the associated * value * @throws IllegalArgumentException if there is an error in * the specification or if the result holder is of an invalid * type. */ public void addOption (String spec, Object resHolder) throws IllegalArgumentException { // null terminated string is easier to parse StringScanner scanner = new StringScanner(spec); Record rec = null; NameDesc nameTail = null; NameDesc ndesc; int i0, i1; char c; do { ndesc = new NameDesc(); boolean nameEndsInWhiteSpace = false; scanner.skipWhiteSpace(); i0 = scanner.getIndex(); while (!Character.isWhitespace(c=scanner.getc()) && c != ',' && c != '%' && c != '\000') ; i1 = scanner.getIndex(); if (c!='\000') { i1--; } if (i0==i1) { // then c is one of ',' '%' or '\000' throw new IllegalArgumentException ("Null option name given"); } if (Character.isWhitespace(c)) { nameEndsInWhiteSpace = true; scanner.skipWhiteSpace(); c = scanner.getc(); } if (c=='\000') { throw new IllegalArgumentException ("No conversion character given"); } if (c != ',' && c != '%') { throw new IllegalArgumentException ("Names not separated by ','"); } ndesc.name = scanner.substring (i0, i1); if (rec == null) { rec = new Record(); rec.nameList = ndesc; } else { nameTail.next = ndesc; } nameTail = ndesc; ndesc.oneWord = !nameEndsInWhiteSpace; } while (c != '%'); if (nameTail == null) { throw new IllegalArgumentException ("Null option name given"); } if (!nameTail.oneWord) { for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) { ndesc.oneWord = false; } } c = scanner.getc(); if (c=='\000') { throw new IllegalArgumentException ("No conversion character given"); } if (validConversionCodes.indexOf(c) == -1) { throw new IllegalArgumentException ("Conversion code '" + c + "' not one of '" + validConversionCodes + "'"); } rec.convertCode = c; if (resHolder instanceof Vector) { rec.vectorResult = true; rec.type = defaultResultType (rec.convertCode); } else { switch (rec.convertCode) { case 'i': case 'o': case 'd': case 'x': { if (resHolder instanceof LongHolder || resHolder instanceof long[]) { rec.type = Record.LONG; } else if (resHolder instanceof IntHolder || resHolder instanceof int[]) { rec.type = Record.INT; } else { throw new IllegalArgumentException ( "Invalid result holder for %" + c); } break; } case 'c': { if (!(resHolder instanceof CharHolder) && !(resHolder instanceof char[])) { throw new IllegalArgumentException ( "Invalid result holder for %c"); } rec.type = Record.CHAR; break; } case 'v': case 'b': { if (!(resHolder instanceof BooleanHolder) && !(resHolder instanceof boolean[])) { throw new IllegalArgumentException ( "Invalid result holder for %" + c); } rec.type = Record.BOOLEAN; break; } case 'f': { if (resHolder instanceof DoubleHolder || resHolder instanceof double[]) { rec.type = Record.DOUBLE; } else if (resHolder instanceof FloatHolder || resHolder instanceof float[]) { rec.type = Record.FLOAT; } else { throw new IllegalArgumentException ( "Invalid result holder for %f"); } break; } case 's': { if (!(resHolder instanceof StringHolder) && !(resHolder instanceof String[])) { throw new IllegalArgumentException ( "Invalid result holder for %s"); } rec.type = Record.STRING; break; } case 'h': { // resHolder is ignored for this type break; } } } if (rec.convertCode == 'h') { rec.resHolder = null; } else { rec.resHolder = resHolder; } scanner.skipWhiteSpace(); // get the range specification, if any if (scanner.peekc() == '{') { if (rec.convertCode == 'h') { throw new IllegalArgumentException ("Ranges not supported for %h"); } // int bcnt = 0; i0 = scanner.getIndex(); // beginning of range spec do { c = scanner.getc(); if (c=='\000') { throw new IllegalArgumentException ("Unterminated range specification"); } // else if (c=='[' || c=='(') // { bcnt++; // } // else if (c==']' || c==')') // { bcnt--; // } // if ((rec.convertCode=='v'||rec.convertCode=='b') && bcnt>1) // { throw new IllegalArgumentException // ("Sub ranges not supported for %b or %v"); // } } while (c != '}'); // if (c != ']') // { throw new IllegalArgumentException // ("Range specification must end with ']'"); // } i1 = scanner.getIndex(); // end of range spec scanRangeSpec (rec, scanner.substring (i0, i1)); if (rec.convertCode == 'v' && rec.rangeList!=null) { rec.vval = rec.rangeList.low.bval; } } // check for value multiplicity information, if any if (scanner.peekc() == 'X') { if (rec.convertCode == 'h') { throw new IllegalArgumentException ("Multipliers not supported for %h"); } scanner.getc(); try { rec.numValues = (int)scanner.scanInt(); } catch (StringScanException e) { throw new IllegalArgumentException ("Malformed value multiplier"); } if (rec.numValues <= 0) { throw new IllegalArgumentException ("Value multiplier number must be > 0"); } } else { rec.numValues = 1; } if (rec.numValues > 1) { for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) { if (ndesc.oneWord) { throw new IllegalArgumentException ( "Multiplier value incompatible with one word option " + ndesc.name); } } } if (resHolder != null && resHolder.getClass().isArray()) { if (Array.getLength(resHolder) < rec.numValues) { throw new IllegalArgumentException ( "Result holder array must have a length >= " + rec.numValues); } } else { if (rec.numValues > 1 && !(resHolder instanceof Vector)) { throw new IllegalArgumentException ( "Multiplier requires result holder to be an array of length >= " + rec.numValues); } } // skip white space following conversion information scanner.skipWhiteSpace(); // get the help message, if any if (!scanner.atEnd()) { if (scanner.getc() != '#') { throw new IllegalArgumentException ("Illegal character(s), expecting '#'"); } String helpInfo = scanner.substring (scanner.getIndex()); // look for second '#'. If there is one, then info // between the first and second '#' is the value descriptor. int k = helpInfo.indexOf ("#"); if (k != -1) { rec.valueDesc = helpInfo.substring (0, k); rec.helpMsg = helpInfo.substring (k+1); } else { rec.helpMsg = helpInfo; } } else { rec.helpMsg = ""; } // parse helpMsg for required/optional information if present // default to required if (rec.helpMsg.indexOf("(optional") != -1) { rec.required = false; } // add option information to match list if (rec.convertCode == 'h' && firstHelpOption == defaultHelpOption) { matchList.remove (defaultHelpOption); firstHelpOption = rec; } matchList.add (rec); } Record lastMatchRecord () { return (Record)matchList.lastElement(); } private Record getRecord (String arg, ObjectHolder ndescHolder) { NameDesc ndesc; for (int i=0; i". The character # acts as * a comment character, causing input to the end of the current line to * be ignored. * * @param reader Reader from which to read the strings * @param args Initial set of argument values. Can be * specified as null. * @throws IOException if an error occured while reading. */ public static String[] prependArgs (Reader reader, String[] args) throws IOException { if (args == null) { args = new String[0]; } LineNumberReader lineReader = new LineNumberReader (reader); Vector vec = new Vector(100, 100); String line; int i, k; while ((line = lineReader.readLine()) != null) { int commentIdx = line.indexOf ("#"); if (commentIdx != -1) { line = line.substring (0, commentIdx); } try { stringToArgs (vec, line, /*allowQuotedStings=*/true); } catch (StringScanException e) { throw new IOException ( "malformed string, line "+lineReader.getLineNumber()); } } String[] result = new String[vec.size()+args.length]; for (i=0; i". The character # acts as a * comment character, causing input to the end of the current line to * be ignored. * * @param file File to be read * @param args Initial set of argument values. Can be * specified as null. * @throws IOException if an error occured while reading the file. */ public static String[] prependArgs (File file, String[] args) throws IOException { if (args == null) { args = new String[0]; } if (!file.canRead()) { return args; } try { return prependArgs (new FileReader (file), args); } catch (IOException e) { throw new IOException ( "File " + file.getName() + ": " + e.getMessage()); } } /** * Sets the parser's error message. * * @param s Error message */ protected void setError (String msg) { errMsg = msg; } /** * Prints an error message, along with a pointer to help options, * if available, and causes the program to exit with code 1. */ public void printErrorAndExit (String msg) { if (helpOptionsEnabled && firstHelpOptionName() != null) { msg += "\nUse "+firstHelpOptionName()+" for help information"; } if (printStream != null) { printStream.println (msg); } System.exit(1); } /** * Matches arguments within an argument list. * *

In the event of an erroneous or unmatched argument, the method * prints a message and exits the program with code 1. * *

If help options are enabled and one of the arguments matches a * help option, then the result of {@link #getHelpMessage * getHelpMessage} is printed to the default print stream and the * program exits with code 0. If help options are not enabled, they * are ignored. * * @param args argument list * @see ArgParser#getDefaultPrintStream */ public void matchAllArgs (String[] args) { matchAllArgs (args, 0, EXIT_ON_UNMATCHED | EXIT_ON_ERROR); } /** * Matches arguments within an argument list and returns * those which were not matched. The matching starts at a location * in args specified by idx, and * unmatched arguments are returned in a String array. * *

In the event of an erroneous argument, the method either prints a * message and exits the program (if {@link #EXIT_ON_ERROR} is * set in exitFlags) * or terminates the matching and creates a error message that * can be retrieved by {@link #getErrorMessage}. * *

In the event of an umatched argument, the method will print a * message and exit if {@link #EXIT_ON_UNMATCHED} is set * in errorFlags. * Otherwise, the unmatched argument will be appended to the returned * array of unmatched values, and the matching will continue at the * next location. * *

If help options are enabled and one of the arguments matches a * help option, then the result of {@link #getHelpMessage * getHelpMessage} is printed to the the default print stream and the * program exits with code 0. If help options are not enabled, then * they will not be matched. * * @param args argument list * @param idx starting location in list * @param exitFlags conditions causing the program to exit. Should be * an or-ed combintion of {@link #EXIT_ON_ERROR} or {@link * #EXIT_ON_UNMATCHED}. * @return array of arguments that were not matched, or * null if all arguments were successfully matched * @see ArgParser#getErrorMessage * @see ArgParser#getDefaultPrintStream */ public String[] matchAllArgs (String[] args, int idx, int exitFlags) { Vector unmatched = new Vector(10); while (idx < args.length) { try { idx = matchArg (args, idx); if (unmatchedArg != null) { if ((exitFlags & EXIT_ON_UNMATCHED) != 0) { printErrorAndExit ( "Unrecognized argument: " + unmatchedArg); } else { unmatched.add (unmatchedArg); } } } catch (ArgParseException e) { if ((exitFlags & EXIT_ON_ERROR) != 0) { printErrorAndExit (e.getMessage()); } break; } } if (unmatched.size() == 0) { return null; } else { return (String[])unmatched.toArray(new String[0]); } } /** * Matches one option starting at a specified location in an argument * list. The method returns the location in the list where the next * match should begin. * *

In the event of an erroneous argument, the method throws * an {@link argparser.ArgParseException ArgParseException} * with an appropriate error message. This error * message can also be retrieved using * {@link #getErrorMessage getErrorMessage}. * *

In the event of an umatched argument, the method will return idx * + 1, and {@link #getUnmatchedArgument getUnmatchedArgument} will * return a copy of the unmatched argument. If an argument is matched, * {@link #getUnmatchedArgument getUnmatchedArgument} will return * null. * *

If help options are enabled and the argument matches a help * option, then the result of {@link #getHelpMessage getHelpMessage} is printed to * the the default print stream and the program exits with code 0. If * help options are not enabled, then they are ignored. * * @param args argument list * @param idx location in list where match should start * @return location in list where next match should start * @throws ArgParseException if there was an error performing * the match (such as improper or insufficient values). * @see ArgParser#setDefaultPrintStream * @see ArgParser#getHelpOptionsEnabled * @see ArgParser#getErrorMessage * @see ArgParser#getUnmatchedArgument */ public int matchArg (String[] args, int idx) throws ArgParseException { unmatchedArg = null; setError (null); try { ObjectHolder ndescHolder = new ObjectHolder(); Record rec = getRecord (args[idx], ndescHolder); if (rec == null || (rec.convertCode=='h' && !helpOptionsEnabled)) { // didn't match unmatchedArg = new String(args[idx]); return idx+1; } NameDesc ndesc = (NameDesc)ndescHolder.value; Object result; if (rec.resHolder instanceof Vector) { result = createResultHolder (rec); } else { result = rec.resHolder; } if (rec.convertCode == 'h') { if (helpOptionsEnabled) { printStream.println (getHelpMessage()); System.exit (0); } else { return idx+1; } } else if (rec.convertCode != 'v') { if (ndesc.oneWord) { rec.scanValue ( result, ndesc.name, args[idx].substring (ndesc.name.length()), 0); } else { if (idx+rec.numValues >= args.length) { throw new ArgParseException ( ndesc.name, "requires " + rec.numValues + " value" + (rec.numValues > 1 ? "s" : "")); } for (int k=0; k 0) // { ps.print (spaceString(initialIndent)); // } // for (int i=0; i"; // if (rec.numValues > 1) // { s += "X" + rec.numValues; // } // } // } // s = s + "]"; // /* // (col+=s.length()) > (maxcols-1) => we will spill over edge. // we use (maxcols-1) because if we go right to the edge // (maxcols), we get wrap new line inserted "for us". // i != 0 means we print the first entry, no matter // how long it is. Subsequent entries are printed // full length anyway. */ // if ((col+=s.length()) > (maxcols-1) && i != 0) // { col = initialIndent+s.length(); // ps.print ("\n" + spaceString(initialIndent)); // } // ps.print (s); // } // if (matchList.size() > 0) // { ps.print ('\n'); // ps.flush(); // } // } /** * Returns a string describing the allowed options * in detail. * * @return help information string. */ public String getHelpMessage () { Record rec; NameDesc ndesc; boolean hasOneWordAlias = false; String s; s = "Usage: " + synopsisString + "\n"; s += "Options include:\n\n"; for (int i=0; i"; } else { optionInfo += "<" + rec.valTypeName() + ">"; } } } if (rec.numValues > 1) { optionInfo += "X" + rec.numValues; } s += optionInfo; if (rec.helpMsg.length() > 0) { int pad = helpIndent - optionInfo.length(); if (pad < 2) { //s += '\n'; pad = helpIndent; } // s += spaceString(pad) + rec.helpMsg; s += spaceString(4) + rec.helpMsg; } s += '\n'; } return s; } /** * Returns the parser's error message. This is automatically * set whenever an error is encountered in matchArg * or matchAllArgs, and is automatically set to * null at the beginning of these methods. * * @return error message */ public String getErrorMessage() { return errMsg; } /** * Returns the value of an unmatched argument discovered {@link * #matchArg matchArg} or {@link #matchAllArgs(String[],int,int) * matchAllArgs}. If there was no unmatched argument, * null is returned. * * @return unmatched argument */ public String getUnmatchedArgument() { return unmatchedArg; } }