// --- 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. * *
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
*
*
-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.
*
*
* parser.addOption ("-v,--verbose %v #print lots of info");
* parser.addOption ("-of,-outfile,-outputFile %s #output file");
*
*
*
* 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 ** *
-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.
*
* 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.
*
* #
* 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
* 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:
* If we need more control over the parsing, we can parse arguments one at
* a time using {@link #matchArg matchArg}:
*
*
* 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 The specification string has the general form
*
* optionNames
*
* where
* Examples:
*
* A range spec of A range spec of A range spec of A range spec of The result holder must be an object capable of holding
* a value compatible with the conversion code,
* or it must be a If the result holder is not a # 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.
*
*
* 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.
*
*
* 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
* %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.
*
* %conversionCode
* [{rangeSpec}]
* [Xmultiplier]
* [#valueDescription]
* [#optionDescription]
*
*
*
* -f, --file).
*
* % character, specifying
* information about what value the option requires:
*
*
*
*
* %fa floating point number
* %ian integer, in either decimal,
* hex (if preceeded by
* 0x), or
* octal (if preceeded by 0)
* %da decimal integer
*
* %oan octal integer
*
* %ha hex integer (without the
* preceeding
* 0x)
* %ca single character, including
* escape sequences (such as \n or \007),
* and optionally enclosed in single quotes
*
* %ba boolean value (
* true
* or false)
* %sa string. This will
* be the argument string itself (or its remainder, in
* the case of a single word option)
*
* %vno explicit value is expected,
* but a boolean value of true (by default)
* will be stored into the associated result holder if this
* option is matched. If one wishes to have a value of
* false stored instead, then the %v
* should be followed by a "range spec" containing
* false, as in %v{false}.
* {2,4,8,16} for an integer
* value will allow the integers 2, 4, 8, or 16.
*
* {[-1.0,1.0]} for a floating
* point value will allow any floating point number in the
* range -1.0 to 1.0.
*
* {(-88,100],1000} for an integer
* value will allow values > -88 and <= 100, as well as 1000.
*
* {"foo", "bar", ["aaa","zzz")} for a
* string value will allow strings equal to "foo" or
* "bar", plus any string lexically greater than or equal
* to "aaa" but less then "zzz".
*
* X character,
* indicating the number of values which the option expects.
* If the multiplier is not specified, it is assumed to be
* 1. If the multiplier value is greater than 1, then the
* result holder should be either an array (of appropriate
* type) with a length greater than or equal to the multiplier
* value, or a java.util.Vector
* as discussed below.
*
* # characters.
* The final # character initiates the
* option description, which may be empty.
* The value description is used in
* generating help messages.
*
* # character
* and the end of the specification string.
* The option description is used in
* generating help messages.
* 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.
*
* 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. * *
%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 |
*
# 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# 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; kmatchArg
* 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;
}
}