package com.netscape.pkisilent.argparser; // --- 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.File; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.io.PrintStream; import java.io.Reader; import java.lang.reflect.Array; import java.util.Vector; /** * 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) { 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 = 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); 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.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 < matchList.size(); i++) { Record rec = (Record) matchList.get(i); for (ndesc = rec.nameList; ndesc != null; ndesc = ndesc.next) { if (rec.convertCode != 'v' && ndesc.oneWord) { if (arg.startsWith(ndesc.name)) { if (ndescHolder != null) { ndescHolder.value = ndesc; } return rec; } } else { if (arg.equals(ndesc.name)) { if (ndescHolder != null) { ndescHolder.value = ndesc; } return rec; } } } } return null; } public void checkRequiredArgs() { for (int i = 1; i < matchList.size(); i++) { Record rec = (Record) matchList.get(i); StringHolder myString = (StringHolder) rec.resHolder; if (((myString.value == null) || (myString.value.equals(""))) && (rec.required)) { printErrorAndExit("Required parameter " + rec.nameList.name + " is not specified."); } } } Object getResultHolder(String arg) { Record rec = getRecord(arg, null); return (rec != null) ? rec.resHolder : null; } String getOptionName(String arg) { ObjectHolder ndescHolder = new ObjectHolder(); Record rec = getRecord(arg, ndescHolder); return (rec != null) ? ((NameDesc) ndescHolder.value).name : null; } String getOptionRangeDesc(String arg) { Record rec = getRecord(arg, null); return (rec != null) ? rec.rangeDesc : null; } String getOptionTypeName(String arg) { Record rec = getRecord(arg, null); return (rec != null) ? rec.valTypeName() : null; } private Object createResultHolder(Record rec) { if (rec.numValues == 1) { switch (rec.type) { case Record.LONG: { return new LongHolder(); } case Record.CHAR: { return new CharHolder(); } case Record.BOOLEAN: { return new BooleanHolder(); } case Record.DOUBLE: { return new DoubleHolder(); } case Record.STRING: { return new StringHolder(); } } } else { switch (rec.type) { case Record.LONG: { return new long[rec.numValues]; } case Record.CHAR: { return new char[rec.numValues]; } case Record.BOOLEAN: { return new boolean[rec.numValues]; } case Record.DOUBLE: { return new double[rec.numValues]; } case Record.STRING: { return new String[rec.numValues]; } } } return null; // can't happen } static void stringToArgs(Vector vec, String s, boolean allowQuotedStrings) throws StringScanException { StringScanner scanner = new StringScanner(s); scanner.skipWhiteSpace(); while (!scanner.atEnd()) { if (allowQuotedStrings) { vec.add(scanner.scanString()); } else { vec.add(scanner.scanNonWhiteSpaceString()); } scanner.skipWhiteSpace(); } } /** * Reads in a set of strings from a reader and prepends them to an * argument list. Strings are delimited by either whitespace or * double quotes ". 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 < vec.size(); i++) { result[i] = (String) vec.get(i); } for (k = 0; k < args.length; k++) { result[i++] = args[k]; } return result; } /** * Reads in a set of strings from a file and prepends them to an * argument list. Strings are delimited by either whitespace or double * quotes ". 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 */ @SuppressWarnings("unchecked") 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 < rec.numValues; k++) { rec.scanValue(result, ndesc.name, args[++idx], k); } } } else { if (rec.resHolder instanceof BooleanHolder) { ((BooleanHolder) result).value = rec.vval; } else { for (int k = 0; k < rec.numValues; k++) { ((boolean[]) result)[k] = rec.vval; } } } if (rec.resHolder instanceof Vector) { ((Vector) rec.resHolder).add(result); } } catch (ArgParseException e) { setError(e.getMessage()); throw e; } return idx + 1; } private String spaceString(int n) { StringBuffer sbuf = new StringBuffer(n); for (int i = 0; i < n; i++) { sbuf.append(' '); } return sbuf.toString(); } // public String getShortHelpMessage () // { // String s; // Record rec; // NameDesc ndesc; // int initialIndent = 8; // int col = initialIndent; // if (maxcols <= 0) // { maxcols = 80; // } // if (matchList.size() > 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 < matchList.size(); i++) { String optionInfo = ""; rec = (Record) matchList.get(i); if (rec.convertCode == 'h' && !helpOptionsEnabled) { continue; } for (ndesc = rec.nameList; ndesc != null; ndesc = ndesc.next) { if (ndesc.oneWord) { hasOneWordAlias = true; break; } } for (ndesc = rec.nameList; ndesc != null; ndesc = ndesc.next) { optionInfo += ndesc.name; if (hasOneWordAlias && !ndesc.oneWord) { optionInfo += " "; } if (ndesc.next != null) { optionInfo += ","; } } if (!hasOneWordAlias) { optionInfo += " "; } if (rec.convertCode != 'v' && rec.convertCode != 'h') { if (rec.valueDesc != null) { optionInfo += rec.valueDesc; } else { if (rec.rangeDesc != null) { optionInfo += "<" + rec.valTypeName() + " " + rec.rangeDesc + ">"; } 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; } }