// $Id: Arguments.java 46 2010-02-02 15:04:53Z gabe.johnson@gmail.com $
//package org.six11.util.args;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
/**
* This parses command line arguments optimized for ease of programmer use. It is NOT a
* swiss-army-knife of command line parsers. It is designed to be easy enough to use and remember
* that your average programmer (e.g. me) can use it without consulting any documentation aside from
* an example.
*
* It accepts boolean-presence short args like -x. It also accepts long arguments like
* (--enable-debugging) followed by an optional word (e.g. --enable-debugging=false). All arguments
* that do not begin with a dash are considered positional.
*
* Order does not matter except for how positional arguments are in relation to one another. So
*
*
* -x --username=billybob myFile
*
*
* is equivalent to
*
*
* --username=billybob myfile -x
*
*
* The following is an example of how to use it in a very simple but powerful way:
*
*
* public static void main(String[] args) {
* Arguments a = new Arguments(args);
* if (a.hasFlag("foo")) {
* System.out.println("You provided the 'foo' flag.");
* } else {
* System.out.println("Maybe try passing in the 'foo' flag.");
* }
* if (a.hasValue("foo")) {
* System.out.println("Huzzah! You provided a value for foo: " + a.getValue("foo"));
* } else {
* System.out.println("You can assign foo a value like this: --foo=blahblah");
* }
* }
*
*
* The following is a more involved example showing how to configure, validate, and use flags.
*
*
* public static void main(String[] args) {
* Arguments a = new Arguments();
*
* a.setProgramName("look"); // set name and documentation for the program as a whole
* a.setDocumentationProgram("Lists files and directories.");
*
* // configure Arguments. Specify which are required, and which take values (e.g. --foo=bar).
* a.addFlag("suffix", ArgType.ARG_OPTIONAL, ValueType.VALUE_REQUIRED,
* "Specify the suffix to show.");
* a.addFlag("h", ArgType.ARG_OPTIONAL, ValueType.VALUE_IGNORED,
* "Show file sizes in a more human-readable form.");
* a.addFlag("l", ArgType.ARG_OPTIONAL, ValueType.VALUE_IGNORED,
* "Long listing. Show many details about a file.");
* a.addFlag("help", ArgType.ARG_OPTIONAL, ValueType.VALUE_IGNORED, "Shows this help.");
* a.addFlag("long-help", ArgType.ARG_OPTIONAL, ValueType.VALUE_IGNORED, "Shows extended help.");
* a.addPositional(0, "dir", ValueType.VALUE_REQUIRED, "The starting directory.");
*
* a.parseArguments(args); // apply rules from above to user-supplied input.
*
* if (a.hasFlag("help")) { // check for --help
* System.out.println(a.getUsage());
* System.exit(0);
* }
*
* if (a.hasFlag("long-help")) { // check for --help
* System.out.println(a.getDocumentation());
* System.exit(0);
* }
*
* try {
* a.validate(); // Ensure user input conforms to our specification and stop if it does not.
* } catch (IllegalArgumentException ex) {
* System.out.println(ex.getMessage());
* System.out.println(a.getUsage());
* System.exit(-1);
* }
*
* // Now we can use the arguments in our simple application that doesn't do anything useful.
* System.out.println("List files in directory " + a.getValue("dir"));
* if (a.hasFlag("l")) {
* System.out.println(" ... use long listing.");
* }
* if (a.hasFlag("h")) {
* System.out.println(" ... use human-readable file sizes.");
* }
* if (a.hasFlag("suffix")) {
* System.out.println(" ... use suffix = '" + a.getValue("suffix") + "'");
* }
* }
*
*
* The second example can be found in Example2.
*
* The Arguments parser can handle arguments in strange orders, and ignores things it does not
* understand. For example:
*
*
* $ ./run org.six11.util.args.Example2 -h --this-flag-is-ignored Monkeychowder bacon --suffix=jpg -l
* List files in directory Monkeychowder
* ... use long listing.
* ... use human-readable file sizes.
* ... use suffix = 'jpg'
*
*
* If you run the program without any arguments it shows you the usage (like --help would):
*
*
* $ ./run org.six11.util.args.Example2
* Wrong number of positional arguments. Expected 1, received 0
* look: Lists files and directories.
* look [ -h -l ] [ --help --long-help --suffix=... ] dir
*
*
* Finally, if you run the above with --long-help it shows you this:
*
*
* $ ./run org.six11.util.args.Example2 --long-help
*
* look: Lists files and directories.
*
* Non-required flags:
*
* h: Show file sizes in a more human-readable form.
* help: Shows this help.
* l: Long listing. Show many details about a file.
* long-help: Shows extended help.
* suffix: Specify the suffix to show. [Must specify value]
*
*Positional fields:
*
* (1) dir (required): The starting directory.
*
*
* @author Gabe Johnson
*/
public class Arguments {
public static enum ArgType {
ARG_OPTIONAL, ARG_REQUIRED
};
public static enum ValueType {
VALUE_OPTIONAL, VALUE_REQUIRED, VALUE_IGNORED
}
private String[] originalArgs;
private Set shortArgs = new HashSet();
private Map longArgs = new HashMap();
private List positionalArgs = new ArrayList();
private Map docs = new HashMap();
private List> positionalDocs = new ArrayList>();
private Set requireFlag = new HashSet();
private Set requireValue = new HashSet();
private int requiredPositionArgs = -1;
private Set optionalFlag = new HashSet();
private Set optionalValue = new HashSet();
private String shortProgramDoc = "";
private String programName = "";
private String space = " ";
/**
* Make a blank Arguments instance suitable for re-use.
*
* Example usage:
*
*
* Arguments args = new Arguments();
* args.addFlag("load-path", ArgType.ARG_OPTIONAL, ValueType.VALUE_REQUIRED,
* "Specifies the root load path for Slippy code.");
* args.parseArguments(userCommandStringArray);
* args.validate(); // throws IllegalArgumentException if something is wrong
* String loadPath = args.hasValue("load-path") ? args.getValue("load-path") : ".";
*
*/
public Arguments() {
// do nothing. Let the programmer configure it first.
}
/**
* Make a new Arguments instance and parse the given arguments. This does not validate them (as
* there are no instructions on how to validate it). This is the fastest way to use Arguments.
* Simply pass your command line input here and as for values using hasFlag(), getValue(), and
* getPosition().
*
* @param args
* the arguments, probably as provided to the main() function.
*/
public Arguments(String[] args) {
parseArguments(args);
}
/**
* Sets a short documentation string for the program. This should be one sentence that tells the
* user what the program does. It should not be an extended discourse.
*/
public void setDocumentationProgram(String pd) {
shortProgramDoc = pd;
}
/**
* Sets the program name---what the user types in to invoke the command.
*/
public void setProgramName(String pn) {
programName = pn;
}
/**
* Documents a given flag.
*/
private void setDocumentation(String flag, String doc) {
// allow a null value, but don't overwrite something existing with a null value.
if (!docs.containsKey(flag) || (docs.containsKey(flag) && doc != null)) {
docs.put(flag, doc);
}
}
private void setDocumentationPositional(int pos, String label, String doc) {
// first ensure there's a spot.
while (positionalDocs.size() <= pos) {
List unknown = new ArrayList();
unknown.add("?");
unknown.add("?");
positionalDocs.add(unknown);
}
positionalDocs.get(pos).set(0, label);
positionalDocs.get(pos).set(1, doc);
}
/**
* Make a String padded on the right with spaces that has the given total length.
*
* Example: makePadded("foo", 6) will return "foo ".
*/
private static String makePadded(String in, int totalLength) {
StringBuilder buf = new StringBuilder();
buf.append(in);
while (buf.length() <= totalLength) {
buf.append(" ");
}
return buf.toString();
}
/**
* Make an ordered list of Strings based on the input, none of which is longer than the given
* length. It assumes the String is broken up by spaces. This is helpful when printing blocks of
* text that can not be too long.
*/
private static List restricLineLength(String in, int length) {
List ret = new ArrayList();
StringBuilder buf = new StringBuilder();
if (in != null) {
StringTokenizer toks = new StringTokenizer(in);
while (toks.hasMoreTokens()) {
String tok = toks.nextToken();
if (buf.length() + tok.length() < length) {
buf.append(" " + tok);
} else {
ret.add(buf.toString().trim());
buf.setLength(0);
buf.append(tok);
}
}
if (buf.length() > 0) {
ret.add(buf.toString().trim());
}
}
return ret;
}
/**
* Gives a short synopsis of how to provide arguments, including which are required and optional,
* and which long arguments take values.
*/
public String getUsage() {
StringBuilder buf = new StringBuilder();
if (programName.length() > 0) {
buf.append(programName + ": ");
}
if (shortProgramDoc.length() > 0) {
buf.append(shortProgramDoc + "\n");
} else {
buf.append("usage synopsis...\n");
}
SortedSet smallRequired = new TreeSet();
SortedSet smallNotRequired = new TreeSet();
SortedSet bigRequired = new TreeSet();
SortedSet bigNotRequired = new TreeSet();
for (String f : docs.keySet()) {
boolean req = requireFlag.contains(f);
boolean sh = f.length() == 1 && !requireValue.contains(f);
if (req && sh) {
smallRequired.add(f);
} else if (req && !sh) {
bigRequired.add(f);
} else if (!req && sh) {
smallNotRequired.add(f);
} else if (!req && !sh) {
bigNotRequired.add(f);
}
}
if (programName.length() > 0) {
buf.append(programName + " ");
}
for (String f : smallRequired) {
buf.append("-" + f + " ");
}
if (smallNotRequired.size() > 0) {
buf.append(" [ ");
for (String f : smallNotRequired) {
buf.append("-" + f + " ");
}
buf.append("] ");
}
for (String f : bigRequired) {
if (requireValue.contains(f)) {
buf.append("--" + f + "=..." + " ");
} else {
buf.append("--" + f + "[=...]" + " ");
}
}
if (bigNotRequired.size() > 0) {
buf.append(" [ ");
for (String f : bigNotRequired) {
if (requireValue.contains(f)) {
buf.append("--" + f + "=..." + " ");
} else {
buf.append("--" + f + " ");
}
}
buf.append("] ");
}
for (int i = 0; i < positionalDocs.size(); i++) {
if (i == requiredPositionArgs) {
buf.append(" [ ");
}
buf.append(positionalDocs.get(i).get(0) + " ");
if (i == requiredPositionArgs) {
buf.append(" ] ");
}
}
return buf.toString();
}
/**
* Returns a verbose String that documents this Arguments instance. It summarizes your options
* into required, non-required, and positional fields. For long args it also tells you which
* fields should have a value if it is present.
*/
public String getDocumentation() {
StringBuilder buf = new StringBuilder("\n");
int maxFlagSize = 0;
SortedSet reqList = new TreeSet();
SortedSet nonReqList = new TreeSet();
if (programName.length() > 0) {
buf.append(programName + ": ");
}
if (shortProgramDoc.length() > 0) {
buf.append(shortProgramDoc + "\n");
} else {
buf.append("Complete documentation...\n");
}
// add flags to required/non-required sets
for (String docMe : docs.keySet()) {
maxFlagSize = Math.max(maxFlagSize, docMe.length());
if (requireFlag.contains(docMe)) {
reqList.add(docMe);
} else {
nonReqList.add(docMe);
}
}
// add positional fields to required/non-required sets
for (int i = 0; i < positionalDocs.size(); i++) {
List posDoc = positionalDocs.get(i);
String pseudoFlag = getPseudoFlag(i, posDoc.get(0));
maxFlagSize = Math.max(maxFlagSize, pseudoFlag.length());
}
if (reqList.size() > 0) {
buf.append("codeTitle>Required flags:\n\n");
buf.append(getDocumentation(reqList, maxFlagSize));
}
if (nonReqList.size() > 0) {
buf.append("\n Non-required flags:\n\n");
buf.append(getDocumentation(nonReqList, maxFlagSize));
}
if (positionalDocs.size() > 0) {
buf.append("\n Positional fields:\n\n");
for (int i = 0; i < positionalDocs.size(); i++) {
List posDoc = positionalDocs.get(i);
String pseudoFlag = getPseudoFlag(i, posDoc.get(0));
buf.append(formatDocumentation(pseudoFlag, space, maxFlagSize, posDoc.get(1), 70));
}
}
return buf.toString();
}
private String getPseudoFlag(int pos, String flag) {
return "(" + (pos + 1) + ") " + flag + ((pos < requiredPositionArgs) ? " (required)" : "");
}
private static String formatDocumentation(String f, String space, int maxFlagSize,
String docString, int maxLineLength) {
StringBuilder buf = new StringBuilder();
buf.append(Arguments.makePadded(f + ":", maxFlagSize));
buf.append(space);
List flagDoc = Arguments.restricLineLength(docString, maxLineLength - maxFlagSize);
String padSpace = Arguments.makePadded("", maxFlagSize + space.length());
for (int i = 0; i < flagDoc.size(); i++) {
if (i > 0) {
buf.append(padSpace);
}
buf.append(flagDoc.get(i) + "\n");
}
if (flagDoc.size() == 0) { // no docs for this one
buf.append("\n");
}
return buf.toString();
}
private String getDocumentation(SortedSet list, int maxFlagSize) {
StringBuilder buf = new StringBuilder();
for (String f : list) {
String docStr = docs.get(f) + (requireValue.contains(f) ? " [Must specify value]" : "");
buf.append(formatDocumentation(f, space, maxFlagSize, docStr, 70));
}
return buf.toString();
}
/**
* Parses arguments. This is how the Arguments object is fed with user-data.
*/
public void parseArguments(String[] args) {
this.originalArgs = args;
for (int i = 0; i < args.length; i++) {
int consumed = parse(i, args);
i = i + consumed;
}
}
public void parseArguments(Arguments original) {
parseArguments(original.getOriginalArgs());
}
/**
* Supplies the string array provided from the command line.
*/
public String[] getOriginalArgs() {
return originalArgs;
}
/**
* Tells you if a given flag was provided.
*/
public boolean hasFlag(String f) {
return shortArgs.contains(f) || longArgs.containsKey(f);
}
/**
* Tells you if the user provided a value for a given flag or documented positional field.
*
* For example, if the user provided --name="Dorp Zirconium", hasValue("name") returns true.
* Alternately, if position 3 was documented with the label "name" and your argument string is
* "foo bar baf", this will also return true (and getValue("name") returns "baf").
*/
public boolean hasValue(String f) {
return longArgs.containsKey(f) && longArgs.get(f) != null;
}
/**
* Returns a value associated with a flag or documented positional field.
*
* @return a String if one was found, or null.
* @see #hasValue(String)
*/
public String getValue(String f) {
return longArgs.get(f);
}
/**
* Tells you how many positional arguments (non-flags) were provided.
*/
public int getPositionCount() {
return positionalArgs.size();
}
/**
* Returns the value of the free position input. For example, if the command line arguments were:
*
* -a Foo --type=jpeg Bar
, getPosition(0) returns Foo and getPosition(1) returns Bar.
*/
public String getPosition(int n) {
return positionalArgs.get(n);
}
/**
* Validates user-provided arguments against the known requirements. Arguments should be provided
* via the Arguments(String[]) constructor, or the parseArguments(String[]) method.
*/
public void validate() {
StringBuilder message = new StringBuilder();
boolean ok = true;
// ensure required flags are here.
for (String requireMe : requireFlag) {
if (!shortArgs.contains(requireMe) && !longArgs.containsKey(requireMe)) {
ok = false;
message.append("Missing Flag: " + requireMe + "\n");
}
}
// ensure flags that are present and require values actually have them.
for (String longPresent : longArgs.keySet()) {
if (requireValue.contains(longPresent) && longArgs.get(longPresent) == null) {
ok = false;
message.append("Missing Value: " + longPresent + " (specify using " + longPresent
+ "=VALUE)");
}
}
if (requiredPositionArgs >= 0 && requiredPositionArgs > positionalArgs.size()) {
ok = false;
message.append("Wrong number of positional arguments. Expected " + requiredPositionArgs
+ ", received " + positionalArgs.size());
}
if (!ok) {
throw new IllegalArgumentException(message.toString());
}
}
private void setRequiredFlag(String f) {
requireFlag.add(f);
}
private void setRequiredValue(String f) {
requireValue.add(f);
}
private void setOptionalFlag(String f) {
optionalFlag.add(f);
setDocumentation(f, null);
}
private void setOptionalValue(String f) {
optionalValue.add(f);
}
private void setRequiredPositionArgs(int n) {
requiredPositionArgs = n;
}
private int parse(int position, String[] args) {
String a = args[position];
int ret = 0;
if (a.startsWith("--")) {
assertOK(a.length() > 2, "Empty long argument in slot " + position);
int eq = a.indexOf('=');
String lval = null;
String rval = null;
if (eq > 0) {
lval = a.substring("--".length(), eq); // --x=y
Arguments.assertOK(a.length() > eq + 1, "Malformed long argument: " + a);
rval = a.substring(eq + 1);
if (rval.startsWith("\"") && !rval.endsWith("\"")) {
boolean complete = false;
for (int i = position + 1; i < args.length; i++) {
rval = rval + " " + args[i];
if (rval.endsWith("\"")) {
complete = true;
ret = i - position;
break;
}
}
Arguments.assertOK(complete, "Unterminated double-quoted string beginning in slot "
+ position + ": " + a);
}
} else {
lval = a.substring("--".length());
}
if (rval != null && rval.startsWith("\"") && rval.endsWith("\"")) {
rval = rval.substring(1, rval.length() - 1);
}
longArgs.put(lval, rval);
} else if (a.startsWith("-")) {
assertOK(a.length() == 2, "Short argument must have one character, e.g. '-x'");
shortArgs.add(a.substring("-".length()));
} else {
positionalArgs.add(a);
if (positionalDocs.size() >= positionalArgs.size()) {
List namedPosition = positionalDocs.get(positionalArgs.size() - 1);
longArgs.put(namedPosition.get(0), a);
}
}
return ret;
}
private static void assertOK(boolean ok, String reason) {
if (!ok) {
System.out.println(reason);
System.exit(-1);
}
}
/**
* Configure the Arguments to understand a given flag. This allows the programmer to call
* 'validate' and ensure the user's arguments match what is expected. It also records
* documentation that is used in getUsage() (a terse summary of the flags) and getDocumentation()
* (which provides all available documentation).
*
* @param flag
* the flag label, without dashes. So if you want your user to type "-h", simply provide
* "h". If you want "--suffix", provide "suffix".
* @param a
* the argument type: either optional or required. See the validate() function.
* @param v
* the value type: either optional or required. See the validate() function. It is valid
* and useful to have a required value for an optional argument. For example, the
* 'username' flag could be optional, but if it is present, a value must be given.
* @param documentation
* The documentation used in getDocumentation()
* @see #validate()
* @see #getDocumentation()
* @see #getUsage()
*/
public void addFlag(String flag, ArgType a, ValueType v, String documentation) {
if (a == ArgType.ARG_OPTIONAL) {
setOptionalFlag(flag);
} else if (a == ArgType.ARG_REQUIRED) {
setRequiredFlag(flag);
}
if (v == ValueType.VALUE_OPTIONAL) {
setOptionalValue(flag);
} else if (v == ValueType.VALUE_REQUIRED) {
setRequiredValue(flag);
}
setDocumentation(flag, documentation);
}
/**
* Configure the Arguments to understand a positional value, which is a bare string without a flag
* in front of it.
*
* @param pos
* The base-0 position. This number is respecitve only to other positional values. So if
* your arguments are "-h -l Monkey --verbose=true Salmon", position 0 is Monkey and
* position 1 is Salmon.
* @param label
* The label can be used later to retrieve this value. For example if your http-get
* program expects a single argument, you can label it 'url', and retrieve it later using
* getValue("url").
* @param v
* The value type. If required, calling 'validate' will complain if it is not present.
* @param documentation
* The documentation string used in getDocumentation().
* @see #getDocumentation()
* @see #getUsage()
* @see #validate()
*/
public void addPositional(int pos, String label, ValueType v, String documentation) {
setDocumentationPositional(pos, label, documentation);
if (v == ValueType.VALUE_REQUIRED) {
setRequiredPositionArgs(Math.max(pos + 1, requiredPositionArgs));
}
}
}