diff options
Diffstat (limited to 'pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java')
-rwxr-xr-x | pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java | 4126 |
1 files changed, 2129 insertions, 1997 deletions
diff --git a/pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java b/pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java index e396d6f5e..a205d1012 100755 --- a/pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java +++ b/pki/base/silent/src/com/netscape/pkisilent/argparser/ArgParser.java @@ -1,5 +1,4 @@ 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 @@ -19,17 +18,17 @@ package com.netscape.pkisilent.argparser; // --- 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. - */ + * 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; @@ -41,200 +40,191 @@ 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 <a href=#rangespec>range - * checking</a>, <a href=#multipleOptionNames>multiple option names</a> - * (aliases), <a href=#singleWordOptions>single word options</a>, <a - * href=#multipleOptionValues>multiple values associated with an option</a>, <a - * href=#multipleOptionInvocation>multiple option invocation</a>, <a - * href=#helpInfo>generating help information</a>, <a - * href=#customArgParsing>custom argument parsing</a>, and <a - * href=#argsFromAFile>reading arguments from a file</a>. The last feature is - * particularly useful and makes it easy to create ad-hoc configuration files - * for an application. - * + * 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 + * <a href=#rangespec>range checking</a>, + * <a href=#multipleOptionNames>multiple option names</a> (aliases), + * <a href=#singleWordOptions>single word options</a>, + * <a href=#multipleOptionValues>multiple values associated with an option</a>, + * <a href=#multipleOptionInvocation>multiple option invocation</a>, + * <a href=#helpInfo>generating help information</a>, + * <a href=#customArgParsing>custom argument parsing</a>, and + * <a href=#argsFromAFile>reading arguments from a file</a>. The + * last feature is particularly useful and makes it + * easy to create ad-hoc configuration files for an application. + * * <h3><a name="example">Basic Example</a></h3> - * - * <p> - * Here is a simple example in which an application has three command line - * options: <code>-theta</code> (followed by a floating point value), - * <code>-file</code> (followed by a string value), and <code>-debug</code>, - * which causes a boolean value to be set. + * + * <p>Here is a simple example in which an application has three + * command line options: + * <code>-theta</code> (followed by a floating point value), + * <code>-file</code> (followed by a string value), and + * <code>-debug</code>, which causes a boolean value to be set. * * <pre> + * + * static public void main (String[] args) + * { + * // create holder objects for storing results ... * - * 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 ... + * DoubleHolder theta = new DoubleHolder(); + * StringHolder fileName = new StringHolder(); + * BooleanHolder debug = new BooleanHolder(); * - * parser.matchAllArgs(args); + * // create the parser and specify the allowed options ... * - * // and print out the values + * 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 ... * - * System.out.println("theta=" + theta.value); - * System.out.println("fileName=" + fileName.value); - * System.out.println("debug=" + debug.value); - * } + * 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); + * } * </pre> - * <p> - * A command line specifying all three options might look like this: - * + * <p>A command line specifying all three options might look like this: * <pre> - * java argparser.SimpleExample -theta 7.8 -debug -file /ai/lloyd/bar + * java argparser.SimpleExample -theta 7.8 -debug -file /ai/lloyd/bar * </pre> * - * <p> - * 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 <code>value</code> field of special ``holder'' - * classes (e.g., {@link argparser.DoubleHolder DoubleHolder}, + * <p>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 <code>value</code> field of special + * ``holder'' classes (e.g., {@link argparser.DoubleHolder DoubleHolder}, * {@link argparser.StringHolder StringHolder}, etc.). - * - * <p> - * 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., - * <code>%f</code> for floating point, <code>%s</code> for a string, - * <code>%v</code> for a boolean flag), and (3) an optional description + * + * <p> 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., <code>%f</code> for floating point, <code>%s</code> for a + * string, <code>%v</code> for a boolean flag), and (3) an optional description * (following the <code>#</code> 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 <a - * href=#multipleOptionInvocation> an instance of <code>java.util.Vector</code> - * </a>. - * - * <p> - * By default, arguments that don't match the specified options, are <a + * 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 + * <a href=#multipleOptionInvocation> an instance of + * <code>java.util.Vector</code></a>. + * + * <p>By default, arguments that don't match the specified options, are <a * href=#rangespec>out of range</a>, or are otherwise formatted incorrectly, - * will cause <code>matchAllArgs</code> 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 <a href=#customArgParsing>processed separately</a> - * + * will cause <code>matchAllArgs</code> 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 + * <a href=#customArgParsing>processed separately</a> + * * <h3><a name="rangespec">Range Specification</a></h3> - * + * * 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 - * <code>-name</code> that expects to be provided with one of three string - * values (<code>john</code>, <code>mary</code>, or <code>jane</code>), an - * option <code>-index</code> that expects to be supplied with a integer value - * in the range 1 to 256, an option <code>-size</code> that expects to be + * conversion code. In the code fragment below, we show how to specify an + * option <code>-name</code> that expects to be provided with one of three + * string values (<code>john</code>, <code>mary</code>, or <code>jane</code>), + * an option <code>-index</code> that expects to be supplied with a integer + * value in the range 1 to 256, an option <code>-size</code> that expects to be * supplied with integer values of either 1, 2, 4, 8, or 16, and an option * <code>-foo</code> that expects to be supplied with floating point values in * the ranges -99 < foo <= -50, or 50 <= foo < 99. - * + * * <pre> - * 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); + * 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); * </pre> - * + * * If an argument value does not lie within a specified range, an error is * generated. - * + * * <h3><a name="multipleOptionNames">Multiple Option Names</a></h3> - * - * An option may be given several names, or aliases, in the form of a comma - * seperated list: - * + * + * An option may be given several names, or aliases, in the form of + * a comma seperated list: + * * <pre> - * parser.addOption("-v,--verbose %v #print lots of info"); - * parser.addOption("-of,-outfile,-outputFile %s #output file"); + * parser.addOption ("-v,--verbose %v #print lots of info"); + * parser.addOption ("-of,-outfile,-outputFile %s #output file"); * </pre> - * + * * <h3><a name="singleWordOptions">Single Word Options</a></h3> - * - * Normally, options are assumed to be "multi-word", meaning that any associated - * value must follow the option as a separate argument string. For example, - * + * + * Normally, options are assumed to be "multi-word", meaning + * that any associated value must follow the option as a + * separate argument string. For + * example, * <pre> - * parser.addOption("-file %s #file name"); + * parser.addOption ("-file %s #file name"); * </pre> - * - * will cause the parser to look for two strings in the argument list of the - * form - * + * will cause the parser to look for two strings in the argument list + * of the form * <pre> * -file someFileName * </pre> - * - * 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, - * + * 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, * <pre> - * parser.addOption("-file=%s #file name"); + * parser.addOption ("-file=%s #file name"); * </pre> - * - * will cause the parser to look for a single string in the argument list of the - * form - * + * will cause the parser to look for a single string in the argument + * list of the form * <pre> * -file=someFileName * </pre> - * * Such an option is called a "single word" option. - * + * * <p> - * 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 + * 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, - * * <pre> - * parser.addOption("-nb=,-number ,-n%d #number of blocks"); + * parser.addOption ("-nb=,-number ,-n%d #number of blocks"); * </pre> - * - * will cause the parser to look for one, two, and one word constructions of the - * forms - * + * will cause the parser to look for one, two, and one word constructions + * of the forms * <pre> * -nb=N * -number N * -nN * </pre> - * + * * <h3><a name="multipleOptionValues">Multiple Option Values</a></h3> - * - * If may be useful for an option to be followed by several values. For - * instance, we might have an option <code>-velocity</code> 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 - * <i>multiplier</i> specification, of the form <code>X</code>N, where N is an - * integer, after the conversion code (or range specification, if present). For - * example, + * + * If may be useful for an option to be followed by several values. + * For instance, we might have an option <code>-velocity</code> + * 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 <i>multiplier</i> specification, + * of the form <code>X</code>N, where N is an integer, + * after the conversion code (or range specification, if present). + * For example, * * <pre> - * double[] pos = new double[3]; - * - * addOption("-position %fX3 #position of the object", pos); + * double[] pos = new double[3]; + * + * addOption ("-position %fX3 #position of the object", pos); * </pre> - * * will cause the parser to look for - * * <pre> * -position xx yy zz * </pre> @@ -242,53 +232,52 @@ import java.util.Vector; * in the argument list, where <code>xx</code>, <code>yy</code>, and * <code>zz</code> are numbers. The values are stored in the array * <code>pos</code>. - * - * Options requiring multiple values must use arrays to return their values, and - * cannot be used in single word format. - * + * + * Options requiring multiple values must use arrays to + * return their values, and cannot be used in single word format. + * * <h3><a name="multipleOptionInvocation">Multiple Option Invocation</a></h3> - * - * 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. - * + * + * 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 <i>all</i> - * values associated with multiple option invocation, by supplying a instance of - * <code>java.util.Vector</code> 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 - * + * values associated with multiple option invocation, by supplying a instance + * of <code>java.util.Vector</code> 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 + * * <pre> - * Vector vec = new Vector(10); - * - * parser.addOption("-foo %f", vec); - * parser.matchAllArgs(args); + * Vector vec = new Vector(10); + * + * parser.addOption ("-foo %f", vec); + * parser.matchAllArgs(args); * </pre> - * * when supplied with an argument list that contains - * * <pre> * -foo 1.2 -foo 1000 -foo -78 * </pre> * * will create three instances of {@link argparser.DoubleHolder DoubleHolder}, - * initialized to <code>1.2</code>, <code>1000</code>, and <code>-78</code>, and - * store them in <code>vec</code>. - * + * initialized to <code>1.2</code>, <code>1000</code>, and <code>-78</code>, + * and store them in <code>vec</code>. + * * <h3><a name="helpInfo">Generating help information</a></h3> - * + * * ArgParser automatically generates help information for the options, and this * information may be printed in response to a <i>help</i> 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 <a - * href=#valueInfo>described below</a>). The application-supplied description is - * whatever appears in the specification string after the optional - * <code>#</code> character. The string returned by {@link #getHelpMessage - * getHelpMessage} for the <a href=#example>first example above</a> would 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 + * <a href=#valueInfo>described below</a>). + * The application-supplied description is whatever + * appears in the specification string after the optional <code>#</code> + * character. The string returned by {@link #getHelpMessage getHelpMessage} for + * the <a href=#example>first example above</a> would be * * <pre> * Usage: java argparser.SimpleExample @@ -302,47 +291,48 @@ import java.util.Vector; * * The options <code>-help</code> and <code>-?</code> 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 <code>%h</code>. Help - * options can be disabled using {@link #setHelpOptionsEnabled + * 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 <code>%h</code>. Help options + * can be disabled using {@link #setHelpOptionsEnabled * setHelpOptionsEnabled(false)}. - * - * <p> - * <a name=valueInfo> A description of the required values for an option can be - * specified explicitly by placing a second <code>#</code> character in the - * specification string. Everything between the first and second <code>#</code> - * characters then becomes the value description, and everything after the - * second <code>#</code> character becomes the option description. For example, - * if the <code>-theta</code> option above was specified with - * + * + * <p><a name=valueInfo> + * A description of the required values for an option can be + * specified explicitly + * by placing a second <code>#</code> character in the specification + * string. Everything between the first and second <code>#</code> + * characters then becomes the value description, and everything + * after the second <code>#</code> character becomes the option + * description. + * For example, if the <code>-theta</code> option + * above was specified with * <pre> - * parser.addOption("-theta %f #NUMBER#theta value (in degrees)", theta); + * parser.addOption ("-theta %f #NUMBER#theta value (in degrees)",theta); * </pre> - * * instead of - * * <pre> - * parser.addOption("-theta %f #theta value (in degrees)", theta); + * parser.addOption ("-theta %f #theta value (in degrees)", theta); * </pre> - * - * then the corresponding entry in the help message would look like - * + * then the corresponding entry in the help message would look + * like * <pre> * -theta NUMBER theta value (in degrees) * </pre> - * + * * <h3><a name="customArgParsing">Custom Argument Parsing</a></h3> * - * 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. - * + * 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. + * * <p> * 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: - * + * matchAllArgs(args,idx,exitFlags)} returns an array of + * all unmatched arguments, which can then be handled + * specially: * <pre> * String[] unmatched = * parser.matchAllArgs (args, 0, parser.EXIT_ON_ERROR); @@ -352,13 +342,12 @@ import java.util.Vector; * </pre> * * For instance, this would be useful for an applicatoon that accepts an - * arbitrary number of input file names. The options can be parsed using - * <code>matchAllArgs</code>, and the remaining unmatched arguments give the - * file names. - * - * <p> - * If we need more control over the parsing, we can parse arguments one at a - * time using {@link #matchArg matchArg}: + * arbitrary number of input file names. The options can be parsed using + * <code>matchAllArgs</code>, and the remaining unmatched arguments + * give the file names. + * + * <p> If we need more control over the parsing, we can parse arguments one at + * a time using {@link #matchArg matchArg}: * * <pre> * int idx = 0; @@ -376,1793 +365,1936 @@ import java.util.Vector; * } * } * </pre> - * + * * {@link #matchArg matchArg(args,idx)} matches one option at location * <code>idx</code> 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. - * + * 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. + * * <h3><a name="argsFromAFile">Reading Arguments From a File</a></h3> - * - * 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 <code>#</code> (which comments out - * everything to the end of the current line). A typical usage looks like this: - * + * + * 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 <code>#</code> (which + * comments out everything to the end of the current line). A typical usage + * looks like this: + * * <pre> * ... create parser and add options ... * * args = parser.prependArgs (new File(".configFile"), args); - * + * * parser.matchAllArgs (args); * </pre> - * - * This makes it easy to generate simple configuration files for an application. - * + * + * This makes it easy to generate simple configuration files for an + * application. + * * @author John E. Lloyd, Fall 2004 */ -public class ArgParser { - Vector<Record> 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 <code>%</code> 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; +public class ArgParser +{ + Vector<Record> 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 <code>%</code> 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 <code>ArgParser</code> with a synopsis + * string, and the default help options <code>-help</code> and + * <code>-?</code>. + * + * @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 <code>ArgParser</code> with a synopsis + * string. The help options <code>-help</code> and + * <code>-?</code> are added if <code>defaultHelp</code> + * 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<Record>(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 + * <p> + * <prec> + * "java somepackage.SomeClass [options] files ..." + * </prec> + * + * <p> 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 <code>%h</code>. 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 <code>true</code>. + * @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. + * + * <p>The specification string has the general form + * + * <p> <var>optionNames</var> + * <code>%</code><var>conversionCode</var> + * [<code>{</code><var>rangeSpec</var><code>}</code>] + * [<code>X</code><var>multiplier</var>] + * [<code>#</code><var>valueDescription</var>] + * [<code>#</code><var>optionDescription</var>] </code> + * + * <p> + * where + * <ul> <p><li><var>optionNames</var> is a + * comma-separated list of names for the option + * (such as <code>-f, --file</code>). + * + * <p><li><var>conversionCode</var> is a single letter, + * following a <code>%</code> character, specifying + * information about what value the option requires: + * + * <table> + * <tr><td><code>%f</code></td><td>a floating point number</td> + * <tr><td><code>%i</code></td><td>an integer, in either decimal, + * hex (if preceeded by <code>0x</code>), or + * octal (if preceeded by <code>0</code>)</td> + * <tr valign=top> + * <td><code>%d</code></td><td>a decimal integer</td> + * <tr valign=top> + * <td><code>%o</code></td><td>an octal integer</td> + * <tr valign=top> + * <td><code>%h</code></td><td>a hex integer (without the + * preceeding <code>0x</code>)</td> + * <tr valign=top> + * <td><code>%c</code></td><td>a single character, including + * escape sequences (such as <code>\n</code> or <code>\007</code>), + * and optionally enclosed in single quotes + * <tr valign=top> + * <td><code>%b</code></td><td>a boolean value (<code>true</code> + * or <code>false</code>)</td> + * <tr valign=top> + * <td><code>%s</code></td><td>a string. This will + * be the argument string itself (or its remainder, in + * the case of a single word option)</td> + * <tr valign=top> + * <td><code>%v</code></td><td>no explicit value is expected, + * but a boolean value of <code>true</code> (by default) + * will be stored into the associated result holder if this + * option is matched. If one wishes to have a value of + * <code>false</code> stored instead, then the <code>%v</code> + * should be followed by a "range spec" containing + * <code>false</code>, as in <code>%v{false}</code>. + * </table> + * + * <p><li><var>rangeSpec</var> is an optional range specification, + * placed inside curly braces, consisting of a + * comma-separated list of range items each specifying + * permissible values for the option. A range item may be an + * individual value, or it may itself be a subrange, + * consisting of two individual values, separated by a comma, + * and enclosed in square or round brackets. Square and round + * brackets denote closed and open endpoints of a subrange, indicating + * that the associated endpoint value is included or excluded + * from the subrange. + * The values specified in the range spec need to be + * consistent with the type of value expected by the option. + * + * <p><b>Examples:</b> + * + * <p>A range spec of <code>{2,4,8,16}</code> for an integer + * value will allow the integers 2, 4, 8, or 16. + * + * <p>A range spec of <code>{[-1.0,1.0]}</code> for a floating + * point value will allow any floating point number in the + * range -1.0 to 1.0. + * + * <p>A range spec of <code>{(-88,100],1000}</code> for an integer + * value will allow values > -88 and <= 100, as well as 1000. + * + * <p>A range spec of <code>{"foo", "bar", ["aaa","zzz")} </code> for a + * string value will allow strings equal to <code>"foo"</code> or + * <code>"bar"</code>, plus any string lexically greater than or equal + * to <code>"aaa"</code> but less then <code>"zzz"</code>. + * + * <p><li><var>multiplier</var> is an optional integer, + * following a <code>X</code> 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 <code>java.util.Vector</code> + * <a href=#vectorHolder>as discussed below</a>. + * + * <p><li><var>valueDescription</var> is an optional + * description of the option's value requirements, + * and consists of all + * characters between two <code>#</code> characters. + * The final <code>#</code> character initiates the + * <i>option description</i>, which may be empty. + * The value description is used in + * <a href=#helpInfo>generating help messages</a>. + * + * <p><li><var>optionDescription</var> is an optional + * description of the option itself, consisting of all + * characters between a <code>#</code> character + * and the end of the specification string. + * The option description is used in + * <a href=#helpInfo>generating help messages</a>. + * </ul> + * + * <p>The result holder must be an object capable of holding + * a value compatible with the conversion code, + * or it must be a <code>java.util.Vector</code>. + * 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 <code>java.util.Vector</code>, + * 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 + * <code>java.util.Vector</code>. + * + * <p>If the result holder is not a <code>Vector</code>, then + * it must correspond as follows to the conversion code: + * + * <table> + * <tr valign=top> + * <td><code>%i</code>, <code>%d</code>, <code>%x</code>, + * <code>%o</code></td> + * <td>{@link argparser.IntHolder IntHolder}, + * {@link argparser.LongHolder LongHolder}, <code>int[]</code>, or + * <code>long[]</code></td> + * </tr> + * + * <tr valign=top> + * <td><code>%f</code></td> + * <td>{@link argparser.FloatHolder FloatHolder}, + * {@link argparser.DoubleHolder DoubleHolder}, + * <code>float[]</code>, or + * <code>double[]</code></td> + * </tr> + * + * <tr valign=top> + * <td><code>%b</code>, <code>%v</code></td> + * <td>{@link argparser.BooleanHolder BooleanHolder} or + * <code>boolean[]</code></td> + * </tr> + * + * <tr valign=top> + * <td><code>%s</code></td> + * <td>{@link argparser.StringHolder StringHolder} or + * <code>String[]</code></td> + * </tr> + * + * <tr valign=top> + * <td><code>%c</code></td> + * <td>{@link argparser.CharHolder CharHolder} or + * <code>char[]</code></td> + * </tr> + * </table> + * + * <p>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. + * + * <p><a name=vectorHolder>If the result holder is a + * <code>Vector</code>, 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. + * + * <p>The object allocated by the system to store the result + * will correspond to the conversion code as follows: + * + * <table> + * <tr valign=top> + * <td><code>%i</code>, <code>%d</code>, <code>%x</code>, + * <code>%o</code></td> + * <td>{@link argparser.LongHolder LongHolder}, or + * <code>long[]</code> if the multiplier value exceeds 1</td> + * </tr> + * + * <tr valign=top> + * <td><code>%f</code></td> + * <td>{@link argparser.DoubleHolder DoubleHolder}, or + * <code>double[]</code> if the multiplier value exceeds 1</td> + * </tr> + * + * <tr valign=top> + * <td><code>%b</code>, <code>%v</code></td> + * <td>{@link argparser.BooleanHolder BooleanHolder}, or + * <code>boolean[]</code> + * if the multiplier value exceeds 1</td> + * </tr> + * + * <tr valign=top> + * <td><code>%s</code></td> + * <td>{@link argparser.StringHolder StringHolder}, or + * <code>String[]</code> + * if the multiplier value exceeds 1</td> + * </tr> + * + * <tr valign=top> + * <td><code>%c</code></td> + * <td>{@link argparser.CharHolder CharHolder}, or <code>char[]</code> + * if the multiplier value exceeds 1</td> + * </tr> + * </table> + * + * @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."); } } - 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 <code>ArgParser</code> with a synopsis string, and the default - * help options <code>-help</code> and <code>-?</code>. - * - * @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 <code>ArgParser</code> with a synopsis string. The help - * options <code>-help</code> and <code>-?</code> are added if - * <code>defaultHelp</code> 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<Record>(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 - * <p> - * <prec> "java somepackage.SomeClass [options] files ..." </prec> - * - * <p> - * 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 <code>%h</code>. 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 <code>true</code>. - * @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. - * - * <p> - * The specification string has the general form - * - * <p> - * <var>optionNames</var> <code>%</code><var>conversionCode</var> [ - * <code>{</code><var>rangeSpec</var><code>}</code>] [<code>X</code> - * <var>multiplier</var>] [<code>#</code><var>valueDescription</var>] [ - * <code>#</code><var>optionDescription</var>] </code> - * - * <p> - * where - * <ul> - * <p> - * <li><var>optionNames</var> is a comma-separated list of names for the - * option (such as <code>-f, --file</code>). - * - * <p> - * <li><var>conversionCode</var> is a single letter, following a - * <code>%</code> character, specifying information about what value the - * option requires: - * - * <table> - * <tr> - * <td><code>%f</code></td> - * <td>a floating point number</td> - * <tr> - * <td><code>%i</code></td> - * <td>an integer, in either decimal, hex (if preceeded by <code>0x</code>), - * or octal (if preceeded by <code>0</code>)</td> - * <tr valign=top> - * <td><code>%d</code></td> - * <td>a decimal integer</td> - * <tr valign=top> - * <td><code>%o</code></td> - * <td>an octal integer</td> - * <tr valign=top> - * <td><code>%h</code></td> - * <td>a hex integer (without the preceeding <code>0x</code>)</td> - * <tr valign=top> - * <td><code>%c</code></td> - * <td>a single character, including escape sequences (such as - * <code>\n</code> or <code>\007</code>), and optionally enclosed in single - * quotes - * <tr valign=top> - * <td><code>%b</code></td> - * <td>a boolean value (<code>true</code> or <code>false</code>)</td> - * <tr valign=top> - * <td><code>%s</code></td> - * <td>a string. This will be the argument string itself (or its remainder, - * in the case of a single word option)</td> - * <tr valign=top> - * <td><code>%v</code></td> - * <td>no explicit value is expected, but a boolean value of - * <code>true</code> (by default) will be stored into the associated result - * holder if this option is matched. If one wishes to have a value of - * <code>false</code> stored instead, then the <code>%v</code> should be - * followed by a "range spec" containing <code>false</code>, as in - * <code>%v{false}</code>. - * </table> - * - * <p> - * <li><var>rangeSpec</var> is an optional range specification, placed - * inside curly braces, consisting of a comma-separated list of range items - * each specifying permissible values for the option. A range item may be an - * individual value, or it may itself be a subrange, consisting of two - * individual values, separated by a comma, and enclosed in square or round - * brackets. Square and round brackets denote closed and open endpoints of a - * subrange, indicating that the associated endpoint value is included or - * excluded from the subrange. The values specified in the range spec need - * to be consistent with the type of value expected by the option. - * - * <p> - * <b>Examples:</b> - * - * <p> - * A range spec of <code>{2,4,8,16}</code> for an integer value will allow - * the integers 2, 4, 8, or 16. - * - * <p> - * A range spec of <code>{[-1.0,1.0]}</code> for a floating point value will - * allow any floating point number in the range -1.0 to 1.0. - * - * <p> - * A range spec of <code>{(-88,100],1000}</code> for an integer value will - * allow values > -88 and <= 100, as well as 1000. - * - * <p> - * A range spec of <code>{"foo", "bar", ["aaa","zzz")} </code> for a string - * value will allow strings equal to <code>"foo"</code> or - * <code>"bar"</code>, plus any string lexically greater than or equal to - * <code>"aaa"</code> but less then <code>"zzz"</code>. - * - * <p> - * <li><var>multiplier</var> is an optional integer, following a - * <code>X</code> 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 <code>java.util.Vector</code> - * <a href=#vectorHolder>as discussed below</a>. - * - * <p> - * <li><var>valueDescription</var> is an optional description of the - * option's value requirements, and consists of all characters between two - * <code>#</code> characters. The final <code>#</code> character initiates - * the <i>option description</i>, which may be empty. The value description - * is used in <a href=#helpInfo>generating help messages</a>. - * - * <p> - * <li><var>optionDescription</var> is an optional description of the option - * itself, consisting of all characters between a <code>#</code> character - * and the end of the specification string. The option description is used - * in <a href=#helpInfo>generating help messages</a>. - * </ul> - * - * <p> - * The result holder must be an object capable of holding a value compatible - * with the conversion code, or it must be a <code>java.util.Vector</code>. - * 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 - * <code>java.util.Vector</code>, 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 <code>java.util.Vector</code>. - * - * <p> - * If the result holder is not a <code>Vector</code>, then it must - * correspond as follows to the conversion code: - * - * <table> - * <tr valign=top> - * <td><code>%i</code>, <code>%d</code>, <code>%x</code>, <code>%o</code></td> - * <td>{@link argparser.IntHolder IntHolder}, {@link argparser.LongHolder - * LongHolder}, <code>int[]</code>, or <code>long[]</code></td> - * </tr> - * - * <tr valign=top> - * <td><code>%f</code></td> - * <td>{@link argparser.FloatHolder FloatHolder}, - * {@link argparser.DoubleHolder DoubleHolder}, <code>float[]</code>, or - * <code>double[]</code></td> - * </tr> - * - * <tr valign=top> - * <td><code>%b</code>, <code>%v</code></td> - * <td>{@link argparser.BooleanHolder BooleanHolder} or - * <code>boolean[]</code></td> - * </tr> - * - * <tr valign=top> - * <td><code>%s</code></td> - * <td>{@link argparser.StringHolder StringHolder} or <code>String[]</code></td> - * </tr> - * - * <tr valign=top> - * <td><code>%c</code></td> - * <td>{@link argparser.CharHolder CharHolder} or <code>char[]</code></td> - * </tr> - * </table> - * - * <p> - * 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. - * - * <p> - * <a name=vectorHolder>If the result holder is a <code>Vector</code>, 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. - * - * <p> - * The object allocated by the system to store the result will correspond to - * the conversion code as follows: - * - * <table> - * <tr valign=top> - * <td><code>%i</code>, <code>%d</code>, <code>%x</code>, <code>%o</code></td> - * <td>{@link argparser.LongHolder LongHolder}, or <code>long[]</code> if - * the multiplier value exceeds 1</td> - * </tr> - * - * <tr valign=top> - * <td><code>%f</code></td> - * <td>{@link argparser.DoubleHolder DoubleHolder}, or <code>double[]</code> - * if the multiplier value exceeds 1</td> - * </tr> - * - * <tr valign=top> - * <td><code>%b</code>, <code>%v</code></td> - * <td>{@link argparser.BooleanHolder BooleanHolder}, or - * <code>boolean[]</code> if the multiplier value exceeds 1</td> - * </tr> - * - * <tr valign=top> - * <td><code>%s</code></td> - * <td>{@link argparser.StringHolder StringHolder}, or <code>String[]</code> - * if the multiplier value exceeds 1</td> - * </tr> - * - * <tr valign=top> - * <td><code>%c</code></td> - * <td>{@link argparser.CharHolder CharHolder}, or <code>char[]</code> if - * the multiplier value exceeds 1</td> - * </tr> - * </table> - * - * @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(); + } + + + 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<String> 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 <code>"</code>. The character <code>#</code> 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 <code>null</code>. + * @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<String> vec = new Vector<String>(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 <code>"</code>. The character <code>#</code> 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 <code>null</code>. + * @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. + * + * <p>In the event of an erroneous or unmatched argument, the method + * prints a message and exits the program with code 1. + * + * <p>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 <code>args</code> specified by <code>idx</code>, and + * unmatched arguments are returned in a String array. + * + * <p>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 <code>exitFlags</code>) + * or terminates the matching and creates a error message that + * can be retrieved by {@link #getErrorMessage}. + * + * <p>In the event of an umatched argument, the method will print a + * message and exit if {@link #EXIT_ON_UNMATCHED} is set + * in <code>errorFlags</code>. + * Otherwise, the unmatched argument will be appended to the returned + * array of unmatched values, and the matching will continue at the + * next location. + * + * <p>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 + * <code>null</code> if all arguments were successfully matched + * @see ArgParser#getErrorMessage + * @see ArgParser#getDefaultPrintStream + */ + public String[] matchAllArgs (String[] args, int idx, int exitFlags) + { + Vector<String> unmatched = new Vector<String>(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. + * + * <p>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}. + * + * <p>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 + * <code>null</code>. + * + * <p>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<Object>)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<matchList.size(); i++) +// { rec = (Record)matchList.get(i); +// s = "["; +// for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) +// { s = s + ndesc.name; +// if (ndesc.oneWord == false) +// { s = s + " "; +// } +// if (ndesc.next != null) +// { s = s + ","; +// } +// } +// if (rec.convertCode != 'v' && rec.convertCode != 'h') +// { if (rec.valueDesc != null) +// { s += rec.valueDesc; +// } +// else +// { s = s + "<" + rec.valTypeName() + ">"; +// 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 <code>matchArg</code> + * or <code>matchAllArgs</code>, and is automatically set to + * <code>null</code> 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, + * <code>null</code> is returned. + * + * @return unmatched argument + */ + public String getUnmatchedArgument() + { + return unmatchedArg; + } +} - // 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<String> 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 - * <code>"</code>. The character <code>#</code> 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 - * <code>null</code>. - * @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<String> vec = new Vector<String>(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 - * <code>"</code>. The character <code>#</code> 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 - * <code>null</code>. - * @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. - * - * <p> - * In the event of an erroneous or unmatched argument, the method prints a - * message and exits the program with code 1. - * - * <p> - * 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 <code>args</code> - * specified by <code>idx</code>, and unmatched arguments are returned in a - * String array. - * - * <p> - * 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 - * <code>exitFlags</code>) or terminates the matching and creates a error - * message that can be retrieved by {@link #getErrorMessage}. - * - * <p> - * In the event of an umatched argument, the method will print a message and - * exit if {@link #EXIT_ON_UNMATCHED} is set in <code>errorFlags</code>. - * Otherwise, the unmatched argument will be appended to the returned array - * of unmatched values, and the matching will continue at the next location. - * - * <p> - * 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 <code>null</code> if - * all arguments were successfully matched - * @see ArgParser#getErrorMessage - * @see ArgParser#getDefaultPrintStream - */ - public String[] matchAllArgs(String[] args, int idx, int exitFlags) { - Vector<String> unmatched = new Vector<String>(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. - * - * <p> - * 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}. - * - * <p> - * 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 - * <code>null</code>. - * - * <p> - * 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<Object>) 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<matchList.size(); i++) - // { rec = (Record)matchList.get(i); - // s = "["; - // for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) - // { s = s + ndesc.name; - // if (ndesc.oneWord == false) - // { s = s + " "; - // } - // if (ndesc.next != null) - // { s = s + ","; - // } - // } - // if (rec.convertCode != 'v' && rec.convertCode != 'h') - // { if (rec.valueDesc != null) - // { s += rec.valueDesc; - // } - // else - // { s = s + "<" + rec.valTypeName() + ">"; - // 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 <code>matchArg</code> or - * <code>matchAllArgs</code>, and is automatically set to <code>null</code> - * 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, <code>null</code> is returned. - * - * @return unmatched argument - */ - public String getUnmatchedArgument() { - return unmatchedArg; - } -} |