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. * *
* 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
* X
N, 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
* 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.
*
*
* 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
* 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
* In the event of an umatched argument, the method will print a message and exit if {@link #EXIT_ON_UNMATCHED} is
* set in
* 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
* 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
* 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#
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%
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%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.
*
* %
conversionCode [{
rangeSpec}
]
* [X
multiplier] [#
valueDescription] [#
* optionDescription]
*
*
*
*
* -f, --file
).
*
* %
character, specifying information
* about what value the option requires:
*
*
*
*
*
*
* %f
a floating point number
*
*
* %i
an integer, in either decimal, hex (if preceeded by
* 0x
), or octal (if preceeded by
* 0
)
*
* %d
a decimal integer
*
*
* %o
an octal integer
*
*
* %h
a hex integer (without the preceeding
* 0x
)
*
* %c
a single character, including escape sequences (such as \n
or \007
), and optionally
* enclosed in single quotes
*
*
* %b
a boolean value (
* true
or false
)
*
* %s
a string. This will be the argument string itself (or its remainder, in the case of a single word option)
*
*
* %v
no 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[]
*
*
* @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
*
*
*
* %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"
. 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"
. 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.
*
* args
specified by idx
, and
* unmatched arguments are returned in a String array.
*
* exitFlags
) or terminates the matching and creates a error message
* that can be retrieved by {@link #getErrorMessage}.
*
* errorFlags
. Otherwise, the unmatched argument will be appended to the returned array of
* unmatched values, and the matching will continue at the next location.
*
* null
if all arguments were successfully matched
* @see ArgParser#getErrorMessage
* @see ArgParser#getDefaultPrintStream
*/
public String[] matchAllArgs(String[] args, int idx, int exitFlags) {
Vectornull
.
*
*