Java tutorial
// $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 * * <pre> * -x --username=billybob myFile * </pre> * * is equivalent to * * <pre> * --username=billybob myfile -x * </pre> * * The following is an example of how to use it in a very simple but powerful way: * * <pre> * 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"); * } * } * </pre> * * The following is a more involved example showing how to configure, validate, and use flags. * * <pre> * 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") + "'"); * } * } * </pre> * * 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: * * <pre> * $ ./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' * </pre> * * If you run the program without any arguments it shows you the usage (like --help would): * * <pre> * $ ./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 * </pre> * * Finally, if you run the above with --long-help it shows you this: * * <pre> * $ ./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. * </pre> * * @author Gabe Johnson <johnsogg@cmu.edu> */ 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<String> shortArgs = new HashSet<String>(); private Map<String, String> longArgs = new HashMap<String, String>(); private List<String> positionalArgs = new ArrayList<String>(); private Map<String, String> docs = new HashMap<String, String>(); private List<List<String>> positionalDocs = new ArrayList<List<String>>(); private Set<String> requireFlag = new HashSet<String>(); private Set<String> requireValue = new HashSet<String>(); private int requiredPositionArgs = -1; private Set<String> optionalFlag = new HashSet<String>(); private Set<String> optionalValue = new HashSet<String>(); private String shortProgramDoc = ""; private String programName = ""; private String space = " "; /** * Make a blank Arguments instance suitable for re-use. * * Example usage: * * <pre> * 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") : "."; * </pre> */ 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<String> unknown = new ArrayList<String>(); 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<String> restricLineLength(String in, int length) { List<String> ret = new ArrayList<String>(); 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<String> smallRequired = new TreeSet<String>(); SortedSet<String> smallNotRequired = new TreeSet<String>(); SortedSet<String> bigRequired = new TreeSet<String>(); SortedSet<String> bigNotRequired = new TreeSet<String>(); 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<String> reqList = new TreeSet<String>(); SortedSet<String> nonReqList = new TreeSet<String>(); 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<String> 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<String> 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<String> 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<String> 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: * * <code>-a Foo --type=jpeg Bar</code>, 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<String> 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)); } } }