From a4682ceae6774956461edd03b2485bbacea445f4 Mon Sep 17 00:00:00 2001 From: mharmsen Date: Tue, 4 Oct 2011 01:17:41 +0000 Subject: Bugzilla Bug #688225 - (dogtagIPAv2.1) TRACKER: of the Dogtag fixes for freeIPA 2.1 git-svn-id: svn+ssh://svn.fedorahosted.org/svn/pki/tags/IPA_v2_RHEL_6_2_20111003@2252 c9f7a03b-bd48-0410-a16d-cbbf54688b0b --- pki/base/silent/src/argparser/ArgParser.java | 2303 ++++++++++++++++++++++++++ 1 file changed, 2303 insertions(+) create mode 100755 pki/base/silent/src/argparser/ArgParser.java (limited to 'pki/base/silent/src/argparser/ArgParser.java') diff --git a/pki/base/silent/src/argparser/ArgParser.java b/pki/base/silent/src/argparser/ArgParser.java new file mode 100755 index 000000000..46251787f --- /dev/null +++ b/pki/base/silent/src/argparser/ArgParser.java @@ -0,0 +1,2303 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- + +/** + * Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, + * copy, modify and redistribute is granted, provided that this copyright + * notice is retained and the author is given credit whenever appropriate. + * + * This software is distributed "as is", without any warranty, including + * any implied warranty of merchantability or fitness for a particular + * use. The author assumes no responsibility for, and shall not be liable + * for, any special, indirect, or consequential damages, or any damages + * whatsoever, arising out of or in connection with the use of this + * software. + */ + +import java.io.PrintStream; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.File; +import java.io.FileReader; +import java.io.Reader; +import java.util.Vector; + +import java.lang.reflect.Array; + +/** + * ArgParser is used to parse the command line arguments for a java + * application program. It provides a compact way to specify options and match + * them against command line arguments, with support for + * range checking, + * multiple option names (aliases), + * single word options, + * multiple values associated with an option, + * multiple option invocation, + * generating help information, + * custom argument parsing, and + * reading arguments from a file. The + * last feature is particularly useful and makes it + * easy to create ad-hoc configuration files for an application. + * + *

Basic Example

+ * + *

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

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

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

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

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

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

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

Range Specification

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

Multiple Option Names

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

Single Word Options

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

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

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

Multiple Option Values

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

Multiple Option Invocation

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

Generating help information

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

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

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

Custom Argument Parsing

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

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

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

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

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

Reading Arguments From a File

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

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

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

The specification string has the general form + * + *

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

+ * where + *

+ * + *

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

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

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

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

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

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

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

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

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

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

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

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

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