/* * Clapi.java * * */ package com.mudchicken.util; import java.io.*; import java.util.*; /** *
Clapi - the Command Line API.
* *A fancy option parser.
* *Clapi has a webpage at * http://www.svincent.com/shawn/software/Clapi/
* *Needs lots more features, some of them might be:
* *XXX bug: suffix, prefix, equals specifiers print out * improperly.
* *XXX bug: should be error if more than one arg arity < 0 specified.
* *Here's a simple example of how to use it.
*
import com.mudchicken.util.Clapi.*;
OptParser optParser = new OptParser ().startClass (MyClass.class);
FileNameOpt inputFileOpt = optParser.opt ().nameless ().required ().
description ("input file").asFileName ();
OptResult optResult = optParser.run (args);
String infileName = inputFileOpt.getString (optResult);
*
*
* Options in Clapi are specified using a builder API.
* *If you have never worked with a builder API before, it might
* be a little mysterious. The trick is that every setter method
* returns a reference to the this pointer, so that
* further setter methods can be directly chained.
For example, in a typical use of OptSpec:
*
* // --- get an option parser
* OptParser optParser = new OptParser ();
*
* // --- make an option.
* StringOpt stringOpt = optParser.opt ().longName ("stringOpt").
* shortName ('s').asString ();
*
*
* In this example, the call to opt() returns an
* instance of OptSpec, which then gets
* longName() called upon it.
* longName() adds a long name and returns the option
* specifier again, so that shortName() can be
* called.
shortName() adds a short name, and returns the
* option specifier again. At this point, asString ()
* is called, which wraps the OptSpec in a StringOpt, adds the
* StringOpt to the parser, and finally, returns the StringOpt.
Builder APIs provide a very compact way of specifying * algorithms. If we hadn't used a builder API, the above code * would have looked like this:
* *
* // --- get an option parser
* OptParser optParser = new OptParser ();
*
* // --- make an option.
* OptSpec stringOptSpec = optParser.opt ();
* stringOptSpec.longName ("stringOpt");
* stringOptSpec.shortName ('s');
*
* StringOpt stringOpt = stringOptSpec.asString ();
*
*
* As you can see, the code is considerably bulkier, and for many * sorts of data structures (particularly those you're building by * hand all the time), a builder API can be a very succinct way of * representing it.
* *Note that builder APIs are not without their drawbacks. * Particularly in a statically typed language, it is often * difficult to combine builder APIs with inheritance hierarchies. * Often, an intermediate 'specification' object must be used. That * approach has been taken with Clapi.
**/ public class Clapi { // ------------------------------------------------------------------------- // ---- Option Parser ---- // ---- ---- // ------------------------------------------------------------------------- /** *The Option Parser.
* *Responsible for creating options and parsing String arrays * into OptResults.
* *There are two ways to invoke the compiler. The first,
* run() does everything for you: it prints usage
* information, calls System.exit() if there is any
* errors, and mostly goes about making itself useful.
For second mode of running, call parse(). If
* there is any error, this method throws a subclass of
* OptParseException. You can deal with this, if you
* like, by calling printUsage, and then taking
* whichever steps you'd like to deal with the error condition.
If there is any problem with the specification of the options
* by you, the programmer, the runtime exception
* OptCompilationFailedException is thrown. This
* exception should probably not be caught and handled. As much as
* possible, static typing has been used to remove sources of errors
* in the specification.
parse or run may be called multiple
* times, and new options may be added to the parser between
* runs.
The parser, on construction, automatically adds the options
* --help, -h, and -?. These
* options all print usage information. There is currently no way
* of supressing this behavior.
Start making a new Option.
* *This method returns an OptSpec instance, which
* has a builder API upon it with which you specify things about
* the option you want to build (i.e. - long & short names,
* description, min/max arities, etc).
When you're done, call one of the
* asType() (i.e. - asInt(),
* asString(), etc) methods to get an
* Opt instance of the appropriate type.
Adds a new option to this OptParser. Normally, you do not
* call this method directly, instead calling opt and
* using the builder API provided.
Defined as running and returning the result of
* parse() with the given tokens.
If there is an OptParseException during the
* execution of parse(), it is caught, usage
* information is printed to System.err, and
* System.exit(1) is called.
Parses the given token array, returning the result of the * parse.
* *If one of the constraints specified when building the
* options is violated, an instance of
* OptParseException is thrown.
Also prints the message from the exception before printing * usage.
* * @param out The PrintWriter to write the usage info to. * @param ex The throwable which caused the usage information * to be printed.. **/ public void printUsage (PrintWriter out, OptParseException ex) { out.println (ex.getMessage ()); // XXX maybe nested exception stuff?? printUsage (out); } /** *Prints usage information for a program based on the * specified options and program information.
* *Prints a banner, summary usage, and option descriptions.
* * @param out The PrintWriter to write the usage info to. **/ public void printUsage (PrintWriter out) { compile (); printBanner (out); out.println (); out.println ("Usage:"); printSummaryUsage (out); out.println (); printOptDescriptions (out); } /** *Prints a pleasant banner describing the program. The * information used for this banner is specified by calls to the * OptParser object.
* * @param out The PrintWriter to write the usage info to. **/ public void printBanner (PrintWriter out) { boolean outputted = false; if (programName != null) { out.print (programName); if (version != null) out.print (' '+version); out.println (); } if (webPage != null) { out.println (webPage); } if (author != null) { out.println (author); } if (copyrightNotice != null) { out.println (copyrightNotice); } if (programDescription != null) wordWrap (out, programDescription, 5, 5); if (!randomQuotes.isEmpty ()) { out.println (); int r = new Random ().nextInt (randomQuotes.size ()); String quote = (String)randomQuotes.get (r); wordWrap (out, quote, 5, 5); } } /** *Prints summary usage information to the given PrintWriter.
* * @param out The PrintWriter to write the usage info to. **/ public void printSummaryUsage (PrintWriter _out) { StringBuffer out = new StringBuffer (); out.append ("java "); if (startClassName != null) out.append (startClassName); else out.append ("Prints descriptions of all the options to the given * writer.
* * @param out The PrintWriter to write the usage info to. **/ public void printOptDescriptions (PrintWriter out) { // --- calculate the longest option int maxOptSpecLength = 0; Iterator it = opts.iterator (); while (it.hasNext ()) { Opt opt = (Opt)it.next (); List optionSpecifiers = opt.getOptionSpecifiers (); Iterator j = optionSpecifiers.iterator (); while (j.hasNext ()) { String optSpec = (String)j.next (); maxOptSpecLength = Math.max (maxOptSpecLength, optSpec.length ()); } } // --- calculate the width of all the columns, based on various stuff. int screenWidth = 78; int rightMargin = 4; int col1Width = maxOptSpecLength+1+1; // COMMMA + SPACE! int col2Width = screenWidth - col1Width - rightMargin; // --- print out all the options, divided into groups. it = optsByGroup.keySet ().iterator (); while (it.hasNext ()) { String groupName = (String)it.next (); List opts = (List)optsByGroup.get (groupName); out.println (); out.println (groupName == null ? "Miscellaneous" : groupName); for (int i=0; iThis method only rebuilds these data structures if the * option specifications are 'dirty': that is, if they have been * changed.
**/ protected void compile () { if (compiled) return; longOpts = new HashMap (); shortOpts = new HashMap (); equalsOpts = new HashMap (); prefixOpts = new HashMap (); suffixOpts = new HashMap (); namelessOpt = null; optsByGroup = new HashMap (); // --- loop through the options. For each one, add keys and // - cross references to the appropriate data structures... Iterator i = opts.iterator (); while (i.hasNext ()) { // XXX check for duplicates. Opt opt = (Opt)i.next (); List longNames = opt.getSpec ().getLongNames (); Iterator j = longNames.iterator (); while (j.hasNext ()) { String name = (String)j.next (); processOption (name, longOpts, opt, "--"+name); } List shortNames = opt.getSpec ().getShortNames (); j = shortNames.iterator (); while (j.hasNext ()) { String name = (String)j.next (); processOption (name, shortOpts, opt, "-"+name); } List equalses = opt.getSpec ().getEqualses (); j = equalses.iterator (); while (j.hasNext ()) { String name = (String)j.next (); processOption (name, equalsOpts, opt, name); } List prefixes = opt.getSpec ().getPrefixes (); j = prefixes.iterator (); while (j.hasNext ()) { String name = (String)j.next (); processOption (name, prefixOpts, opt, name+"*"); } List suffixes = opt.getSpec ().getSuffixes (); j = suffixes.iterator (); while (j.hasNext ()) { String name = (String)j.next (); processOption (name, suffixOpts, opt, "*"+name); } if (opt.getSpec ().getNameless ()) if (namelessOpt != null) throw new OptCompilationFailedException ("Only one nameless option allowed at a time."); else namelessOpt = opt; // --- process groups. List groups = opt.getSpec ().getGroups (); if (groups.isEmpty ()) { groups = new ArrayList (); groups.add (null); } j = groups.iterator (); while (j.hasNext ()) { String name = (String)j.next (); List groupList = (List)optsByGroup.get (name); if (groupList == null) { groupList = new ArrayList (); optsByGroup.put (name, groupList); } groupList.add (opt); } } compiled = true; } /** * Utility method for use by compile method. **/ private void processOption (String name, Map set, Opt opt, String debugName) { if (set.containsKey (name)) throw new OptCompilationFailedException (debugName+" specified more than once!"); set.put (name, opt); } /** *Process '@' options, loading sets of options from named * files.
* * @param tokens The tokens to process * @return The given tokens, with @ options expanded. * * @exception OptParseException * Thrown if there is an error loading or parsing an * @-option file. **/ protected String[] preprocessTokens (String[] tokens) throws OptParseException { List result = new ArrayList (); for (int i=0; inull, if
* there is no match.
**/
protected Opt matchNamelessToken (String token)
{
Iterator i;
i = equalsOpts.keySet ().iterator ();
while (i.hasNext ())
{
String equals = (String)i.next ();
if (token.equals (equals)) return (Opt)equalsOpts.get (equals);
}
i = prefixOpts.keySet ().iterator ();
while (i.hasNext ())
{
String prefix = (String)i.next ();
if (token.startsWith (prefix)) return (Opt)prefixOpts.get (prefix);
}
i = suffixOpts.keySet ().iterator ();
while (i.hasNext ())
{
String suffix = (String)i.next ();
if (token.endsWith (suffix)) return (Opt)suffixOpts.get (suffix);
}
return namelessOpt;
}
// -----------------------------------------------------------------------
// ---- Read a token file ------------------------------------------------
// -----------------------------------------------------------------------
/**
* Load the named @-option file into a List.
* * @param fileName The name of the file to load. * @return A List containing the options loaded from the file. * * @exception OptParseException * Thrown if there is an error loading or parsing * the file. **/ protected List loadTokenFile (String fileName) throws OptParseException, IOException { List result = new ArrayList (); StreamTokenizer in = new StreamTokenizer (new FileReader (fileName)); in.resetSyntax (); in.wordChars ('A', 'Z'); in.wordChars ('a', 'z'); in.wordChars ('\u0000', '\uffff'); in.whitespaceChars ('\u0000', '\u0020'); in.commentChar ('/'); in.quoteChar ('\''); in.quoteChar ('"'); in.slashSlashComments (true); in.slashStarComments (true); int token = in.nextToken (); while (token != StreamTokenizer.TT_EOF) { switch (token) { case '"': case '\'': case StreamTokenizer.TT_WORD: result.add (in.sval); break; default: throw new OptParseException (fileName+":"+in.lineno ()+": unexpected token type "+token); } token = in.nextToken (); } return result; } // ----------------------------------------------------------------------- // ---- Word-wrap algorithms --------------------------------------------- // ----------------------------------------------------------------------- /** *Word-wrap the specified text to an 80-column screen.
* * @see #wordWrap(PrintWriter,String,int,int,int) **/ public static void wordWrap (PrintWriter out, String t, int leftMargin, int rightMargin) { wordWrap (out, t, leftMargin, rightMargin, 80); } /** *Word-wrap the specified text to a PrintWriter.
* *The algorithm used here is very expensive, and so has * limited applicability. For use in a usage printing scenario, * this is fine. Simplicity and straightforwardness matters more * to me here than screaming performance while printing how to use * grep.
* * @param out the PrintWriter used to wrap words * @param text the text to print. * @param leftMargin the left margin (in characters) * @param rightMargin the right margin (in characters) * @param pageWidth the width of the page (in characters) **/ public static void wordWrap (PrintWriter out, String text, int leftMargin, int rightMargin, int pageWidth) { // --- note that the -1 factor in the 'col 1 width' calculation is // - to avoid characters being printed in the last column, which // - on many displays forces an implicit linefeed. List words = breakIntoWords (text); printTwoColumns (out, words, // col 1 words new ArrayList (), // col 2 words leftMargin, // left margin pageWidth - rightMargin - 1, // col 1 width 0, // col 2 width false, // no commas col 1 false); // no commas col 2 } /** * Breaks the given string into words. Used in support of *wordWrap and friends.
**/
public static List breakIntoWords (String s)
{
List words = new ArrayList ();
StringTokenizer tokenizer = new StringTokenizer (s, " ");
while (tokenizer.hasMoreTokens ())
{
String word = tokenizer.nextToken ();
words.add (word);
}
return words;
}
/**
* Given 2 lists of strings, lay them out in two columns, pleasantly.
**/
private static void printTwoColumns (PrintWriter out,
List firstCol, List secondCol,
int rightMargin,
int maxCol1Width, int maxCol2Width,
boolean col1Commas,
boolean col2Commas)
{
ListIterator first = firstCol.listIterator ();
ListIterator second = secondCol.listIterator ();
while (first.hasNext () || second.hasNext ())
{
for (int i=0; iAn option specifier provides a builder API for specifying the * features of an option.
**/ public static class OptSpec { OptParser parser; List longNames = new ArrayList (); List shortNames = new ArrayList (); List equalses = new ArrayList (); List prefixes = new ArrayList (); List suffixes = new ArrayList (); boolean nameless = false; List groups = new ArrayList (); String description = null; boolean required = false; int minArity = 0; int maxArity = 1; int argumentArity = 1; public OptSpec (OptParser _parser) { parser = _parser; } /** Wraps this specification as a Help option, adds it to the parser, and returns the Help option. */ public HelpOpt asHelp () { return (HelpOpt)parser.addOpt (new HelpOpt (this)); } /** Wraps this specification as a Boolean option, adds it to the parser, and returns the Boolean option. */ public BooleanOpt asBoolean () { return (BooleanOpt)parser.addOpt (new BooleanOpt (this)); } /** Wraps this specification as a String option, adds it to the parser, and returns the String option. */ public StringOpt asString () { return (StringOpt)parser.addOpt (new StringOpt (this)); } /** Wraps this specification as a Int option, adds it to the parser, and returns the Int option. */ public IntOpt asInt () { return (IntOpt)parser.addOpt (new IntOpt (this)); } /** Wraps this specification as a FileName option, adds it to the parser, and returns the FileName option. */ public FileNameOpt asFileName () { return (FileNameOpt)parser.addOpt (new FileNameOpt (this)); } /** Adds a long name to this option spec. */ public OptSpec longName (String v) { longNames.add (v); return this; } /** Adds a short name to this option spec. */ public OptSpec shortName (char v) { shortNames.add (String.valueOf (v)); return this; } /** *Adds a equals specifier to this option spec.
* *An equals specifier says that if a particular string is * found on the command line, it is to be processed by a * particular option.
**/ public OptSpec equals (String v) { equalses.add (v); return this; } /** *Adds a prefix specifier to this option spec.
* *An prefix specifier says that if a string with a particular * prefix is found on the command line, it is to be processed by a * particular option.
**/ public OptSpec prefix (String v) { prefixes.add (v); return this; } /** *Adds a suffix specifier to this option spec.
* *An suffix specifier says that if a string with a particular * suffix is found on the command line, it is to be processed by a * particular option.
**/ public OptSpec suffix (String v) { suffixes.add (v); return this; } /** *Specifies that this option will be nameless.
* *There may only be one nameless option. If an option is * encountered which has no name (no short name or long name), and * does not match any of the suffix, prefix, or equals rules), * then it is processed by the nameless option, if it is * available. Nameless options are often used for utilities which * process a number of filenames, which may be arbitrary * strings.
**/ public OptSpec nameless () { nameless = true; return this; } /** *Specifies this option's minimum arity. It is a parse error
* for the option opt to be specified fewer than
* opt.getMinArity() times.
Some min arities of note are 0 (which means that the option * is optional), and 1 (which means that the option is * required).
* *The default minArity of any option is 0, meaning that it is * optional.
* * @see #required * @see #optional **/ public OptSpec minArity (int v) { minArity = v; return this; } /** *Specifies this option's maximum arity. It is a parse error
* for the option opt to be specified more than
* opt.getMaxArity() times.
There is also special handling for values less than 0. If * the maximuum arity is -1 or lower, then the option may be * specified as many times as the user likes, and there will be no * error.
* *Some min arities of note are 0 (which means that the option * cannot be specified -- this is a stupid thing to set max arity * to), 1 (which means that the option can only be specified once * -- it is a singleton), and -1 (which means that the option can * be specified as many times as the user would like.)
* *The default maxArity of any option is 1, meaning that it * cannot be specified more than once.
* * @see #list **/ public OptSpec maxArity (int v) { maxArity = v; return this; } /** *Specifies the number of arguments this parameter should receive.
* *If 0 arguments are specified,
**/ public OptSpec argumentArity (int v) { argumentArity = v; return this; } /** Specifies this option's description: used in usage information. */ public OptSpec description (String v) { description = v; return this; } /** * Specifies this option's string group name. When usage * information is printed out, options are grouped into these * groups, and the group names are used as headers. **/ public OptSpec group (String groupName) { groups.add (groupName); return this; } /** alias for minArity (0) */ public OptSpec optional () { return minArity (0); } /** alias for minArity (1) */ public OptSpec required () { return minArity (1); } /** alias for maxArity (-1) */ public OptSpec list () { return maxArity (-1); } public List getLongNames () { return longNames; } public List getShortNames () { return shortNames; } public List getEqualses () { return equalses; } public List getPrefixes () { return prefixes; } public List getSuffixes () { return suffixes; } public boolean getNameless () { return nameless; } public String getDescription () { return description; } public boolean getRequired () { return minArity > 0; } public int getMinArity () { return minArity; } public int getMaxArity () { return maxArity; } public int getArgumentArity () { return argumentArity; } /** Return true if this option must be specified. */ public boolean isRequired () { return getMinArity () > 0; } /** Return true if this option may be specified more than once. */ public boolean isList () { return getMaxArity () > 1 || getMaxArity () < 0; } /** Return the list of groups this option is a member of. */ public List getGroups () { return groups; } } public abstract static class ArgumentParser { Opt opt; public ArgumentParser (Opt _opt) { opt = _opt; } public Opt getOpt () { return opt; } public OptSpec getSpec () { return opt.getSpec (); } protected abstract Object parseArguments (TokenStream in) throws OptParseException; public String getTypeName () { return "argument"; } public void printArgumentsSpec (PrintWriter out) { if (getSpec ().getArgumentArity () < 0) out.print ("<"+getTypeName ()+">..."); else { for (int i=0; iThis is the main deal: users of Clapi create instances of * Opt subclasses, and ask the parser to parse. The subclasses of * Opt are responsible for parsing their arguments and constructing * the result data structure.
**/ public abstract static class Opt { OptSpec spec; public Opt (OptSpec _spec) { spec = _spec; } public OptSpec getSpec () { return spec; } public abstract ArgumentParser getArgumentParser (); /** *Parses the arguments for this option, adding them to the * OptResult data structure passed in.
* *Consumes tokens from the input stream.
**/ protected void parseArguments (TokenStream in, OptResult optResult) throws OptParseException { Object result = getArgumentParser ().parseArguments (in); optResult.add (this, result); } /** * Grab the given token. If the token specified is outside of the * token stream, throw a parse exception. **/ protected String getToken (TokenStream in) throws OptParseException { if (!in.hasNext ()) throw new OptParseException ("Option '"+tag ()+"' expected token, got end of options!"); return in.nextToken (); } protected void checkArity (OptResult result) throws OptParseException { Object value = result.get (this); int minArity = getSpec ().getMinArity (); if (minArity > 0 && !result.containsKey (this)) throw new OptParseException ("Required opt "+tag ()+" not specified"); if (minArity > 1) { // --- it's a list. if (!(value instanceof List)) throw new OptParseException ("For opt "+tag ()+" minimum arity "+ "is greater than 1, and somehow we "+ "got a non-list!!"); List list = (List)value; if (list.size () < minArity) throw new OptParseException ("Required at least "+minArity+ " repetitions of opt "+tag ()+ ", got "+list.size ()+" repetitions"); } } public Object get (OptResult result) { return result.get (this); } public List getList (OptResult result) { // XXX error if maxArity <= 1?? return (List)result.get (this); } public Object[] getObjectArray (OptResult result) { List l = getList (result); if (l == null) return null; return l.toArray (new Object[l.size ()]); } public String toString () { return tag (); } public String tag () { List optionSpecifiers = getOptionSpecifiers (); if (optionSpecifiers.isEmpty ()) return "