I’ve talked before on this blog about Apache Commons CLI project and showcased how easy it is to use it to build a rather complex command-line syntax for your Java application. If you find yourself at any point writing an application which needs more than one command line switch, I strongly recommend Commons CLI is the way to go: it takes care of basic “syntax” validation, missing command-line parameters, help screens and so on, and all of this in exchange of pretty much providing a structured set of options describing the parameters expected / accepted by your application.
One of the problems when dealing with command-line arguments is that in a lot of cases the application needs to interpret these not as simple strings but parse them in some different data types and interpret them as numbers, file names, url’s etc. A classic example is specifying the max heap space when starting a java process:
java -Xmx512m ...
In the above case, the java process needs to interpret the argument after -Xmx as a number followed by a “denominator” (e.g. kilobytes, megabytes etc) — as such will employ a process of extracting the digits in a separate string, parsing them, ensure they are a valid number, then separately identify the denominator (“m” in this case), ensure it’s a valid one and find the multiplier (megabytes in this case – i.e. 1024 X 1Kb) and based on these 2 finally compute the final result.
In such cases, typically you would still employ Commons CLI, but have to spend some effort and code interpreting these parameters, validate them and convert them to the required type (be it integer, float, etc).
Let’s consider an example where your application has to read from the command line an integer, a long, a float and a double value. Your boilerplate code would look something like this:
/** * Example of "old school" CLI usage to parse numbers. * * @author Liviu Tudor http://about.me/liviutudor */ public class CliTypesParseNumber { /** * Program entry point. * * @param args * Command-line arguments */ public static void main(String[] args) { /* Parse command line parameters first */ CommandLineParser parser = new PosixParser(); HelpFormatter help = new HelpFormatter(); CommandLine cmd = null; Options options = buildOptions(); Parameters p = null; try { cmd = parser.parse(options, args); p = validateParameters(cmd); } catch (ParseException e) { System.err.println("Wrong parameters:" + e.getMessage()); help.printHelp("aws-version-mgmt", options); System.exit(1); } // p is definitely not null we got here System.out.println("Parameters parsed: " + p); } /** * Builds the options supported in the command line to be used by parser. * * @return Options for the parser */ @SuppressWarnings("static-access") static Options buildOptions() { Options o = new Options(); o.addOption(OptionBuilder.hasArg().withArgName("integer value").withType(Number.class).withDescription("Specify an integer value").create(OPT_INT)); o.addOption(OptionBuilder.hasArg().withArgName("long value").withType(Number.class).withDescription("Specify a long value").create(OPT_LONG)); o.addOption(OptionBuilder.hasArg().withArgName("double value").withType(Number.class).withDescription("Specify a double value").create(OPT_DOUBLE)); o.addOption(OptionBuilder.hasArg().withArgName("float value").withType(Number.class).withDescription("Specify a float value").create(OPT_FLOAT)); return o; } static Parameters validateParameters(CommandLine cmd) throws ParseException { Parameters p = new Parameters(); /* NUMBERS */ // int String s = cmd.getOptionValue(OPT_INT); int intValue; try { intValue = Integer.parseInt(s); p.setIntValue(intValue); } catch (NumberFormatException e) { throw new ParseException("Not an integer value " + s); } // long s = cmd.getOptionValue(OPT_LONG); long longValue; try { longValue = Long.parseLong(s); p.setLongValue(longValue); } catch (NumberFormatException e) { throw new ParseException("Not a long value " + s); } // float s = cmd.getOptionValue(OPT_FLOAT); float floatValue; try { floatValue = Float.parseFloat(s); p.setFloatValue(floatValue); } catch (NumberFormatException e) { throw new ParseException("Not a float value " + s); } // double s = cmd.getOptionValue(OPT_DOUBLE); double doubleValue; try { doubleValue = Double.parseDouble(s); p.setDoubleValue(doubleValue); } catch (NumberFormatException e) { throw new ParseException("Not a double value " + s); } // if all goes well returned the parsed parameters return p; } } |
As you can see, part of the code deals with trying to parse each parameter as a certain numeric type and failing that throws an exception — this then gets caught and the user is prompted with an error message (and help screen). One of the problems is though your “business logic” (if you can call the above “logic”!) code gets nastily decorated with a lot of code required for parsing and validating these values.
Luckily, CLI’s CommandLine offers a little-known method called getParsedOptionValue() which can take some of this pain and messy code away! Unfortunately there isn’t much documentation around this, so I had to dig into the source code to figure out what’s needed to use this.
The answer lies in the class PatternOptionBuilder — this class is used as a builder for an Option object, to be passed to the parser. In the above code you can see a typical usage of this:
o.addOption(OptionBuilder.hasArg().withArgName("integer value")... |
One of the interesting (yet not so documented!) functions is withType(). The signature of this function indicates it takes an Object
parameter — which is pretty vague unfortunately. The JavaDoc doesn’t help either :
The next Option created will have a value that will be an instance of type.
There’s nothing there to actually indicate how this works 🙁 So like I said, I had to dig into the OptionBuilder
and PatternOptionBuilder
source code to figure out what’s the story.
It turns out that the parameter expected in withType()
is actually an instance of type Class
! In other words, the type expected for an option is the same as the class expected for an option! The next question is of course what are the classes that can be supplied to this function?
This is where the source code for PatternOptionBuilder
helps:
public class PatternOptionBuilder { /** String class */ public static final Class STRING_VALUE = String.class; /** Object class */ public static final Class OBJECT_VALUE = Object.class; /** Number class */ public static final Class NUMBER_VALUE = Number.class; /** Date class */ public static final Class DATE_VALUE = Date.class; /** Class class */ public static final Class CLASS_VALUE = Class.class; /// can we do this one?? // is meant to check that the file exists, else it errors. // ie) it's for reading not writing. /** FileInputStream class */ public static final Class EXISTING_FILE_VALUE = FileInputStream.class; /** File class */ public static final Class FILE_VALUE = File.class; /** File array class */ public static final Class FILES_VALUE = File[].class; /** URL class */ public static final Class URL_VALUE = URL.class; |
Looking further into this, it turns out these values are used for type validation. Unfortunately, again, very little documentation around it, so I had to try it out.
For instance, using the Number
class is a bit vague — does that only work for integer values? Does it include support for floating point numbers too? What about using the File
class? Does it verify if the file exists? So I set off to write some basic code and figure these out and here’s what I found:
- Supplying
Number.class
towithType()
means that the parser will try to first parse the value as aLong
(and return an instace ofjava.lang.Long
). Failing that, it will then attempt to parse it as aDouble
— and if that fails too thegetParsedOptionValue()
method will throw aParseException
. There is no way to get anInteger
orFloat
back so your code has to convert fromlong
toint
ordouble
tofloat
. - Using a
File.class
doesn’t really do much apart from returning aFile
object created using the parameter value. You can of course then call the “usual” functions on this object (isDirectory(), exists()
) etc, however the parser doesn’t perform any validation around it (i.e. correct file name, file exists, etc) FileInputStream.class
doesn’t work unfortunately the way I thought it should. I am using Commons CLI 1.2 and trying to useFileInputStream
inwithType()
in order to validate that the file exists (which is what the source comment suggests). Instead you get still an instance ofFile
class and no validation as to whether it’s a file, whether the file exists and so on is performed.- Using an
URL
class causes the parser to verify the argument passed is a well-constructed URL (supportshttp://, ftp://
) — however, as to be expected really, it does not verify that the domain exists. So you will get an instance ofURL
but trying to perform actions on it might still fail if the domain name is wrong or down or so on. Date
is not working — and trying to invokegetParsedOptionValue()
for an option which was built viawithType(Date.class)
throws anUnsupportedOperationException
. Again, this is what’s happening with version 1.2 of Commons CLI — hopefully future versions should fix that?
Now, having established this, the code to parse and retrieve an option using the types above becomes much cleaner — in the following example, I’ve created a simple Java bean class called Parameters
which has a member of each type mentioned above:
import java.io.File; import java.io.FileInputStream; import java.net.URL; import java.util.Date; /** * Just a simple encapsulation of (parsed) command line parameters. * * @author Liviu Tudor http://about.me/liviutudor */ public class Parameters { private int intValue; private long longValue; private float floatValue; private double doubleValue; private Class<?> classValue; private File fileValue; private FileInputStream fis; private URL url; private Date date; public final int getIntValue() { return intValue; } public final void setIntValue(int intValue) { this.intValue = intValue; } public final long getLongValue() { return longValue; } public final void setLongValue(long longValue) { this.longValue = longValue; } public final float getFloatValue() { return floatValue; } public final void setFloatValue(float floatValue) { this.floatValue = floatValue; } public final double getDoubleValue() { return doubleValue; } public final void setDoubleValue(double doubleValue) { this.doubleValue = doubleValue; } public final Class<?> getClassValue() { return classValue; } public final void setClassValue(Class<?> classValue) { this.classValue = classValue; } public final File getFileValue() { return fileValue; } public final void setFileValue(File fileValue) { this.fileValue = fileValue; } public final FileInputStream getFis() { return fis; } public final void setFis(FileInputStream fis) { this.fis = fis; } public final URL getUrl() { return url; } public final void setUrl(URL url) { this.url = url; } public final Date getDate() { return date; } public final void setDate(Date date) { this.date = date; } @Override public String toString() { return "Parameters [intValue=" + intValue + ", longValue=" + longValue + ", floatValue=" + floatValue + ", doubleValue=" + doubleValue + ", classValue=" + classValue + ", fileValue=" + fileValue + ", fis=" + fis + ", url=" + url + ", date=" + date + "]"; } } |
Using the “boilerplate” code in the first example, parsing these arguments become as simple as this:
import java.io.File; import java.io.FileInputStream; import java.net.URL; import java.util.Date; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; /** * Shows the usage of <a href="http://commons.apache.org/proper/commons-cli/" * target="_blank">Apache Commons CLI</a> with typed arguments. * * @author Liviu Tudor http://about.me/liviutudor */ public class CliTypes { /* === SUPPORTED PARAMS === */ static final String OPT_INT = "int"; static final String OPT_LONG = "long"; static final String OPT_DOUBLE = "dbl"; static final String OPT_FLOAT = "flt"; static final String OPT_CLASS = "cls"; static final String OPT_FILE = "file"; static final String OPT_EXISTING_FILE = "e"; static final String OPT_URL = "url"; static final String OPT_DATE = "date"; /** * Program entry point. * * @param args * Command line arguments. Get parsed and validated. */ public static void main(String[] args) { /* Parse command line parameters first */ CommandLineParser parser = new PosixParser(); HelpFormatter help = new HelpFormatter(); CommandLine cmd = null; Options options = buildOptions(); Parameters p = null; try { cmd = parser.parse(options, args); p = validateParameters(cmd); } catch (ParseException e) { System.err.println("Wrong parameters:" + e.getMessage()); help.printHelp("aws-version-mgmt", options); System.exit(1); } // p is definitely not null we got here System.out.println("Parameters parsed: " + p); } /** * Builds the options supported in the command line to be used by parser. * * @return Options for the parser */ @SuppressWarnings("static-access") static Options buildOptions() { Options o = new Options(); o.addOption(OptionBuilder.hasArg().withArgName("integer value").withType(Number.class) .withDescription("Specify an integer value").create(OPT_INT)); o.addOption(OptionBuilder.hasArg().withArgName("long value").withType(Number.class) .withDescription("Specify a long value").create(OPT_LONG)); o.addOption(OptionBuilder.hasArg().withArgName("double value").withType(Number.class) .withDescription("Specify a double value").create(OPT_DOUBLE)); o.addOption(OptionBuilder.hasArg().withArgName("float value").withType(Number.class) .withDescription("Specify a float value").create(OPT_FLOAT)); o.addOption(OptionBuilder.hasArg().withArgName("class").withType(Class.class) .withDescription("Specify a class name").create(OPT_CLASS)); o.addOption(OptionBuilder.hasArg().withArgName("file path").withType(File.class) .withDescription("Specify a file path").create(OPT_FILE)); o.addOption(OptionBuilder.hasArg().withArgName("file path").withType(FileInputStream.class) .withDescription("Specify an EXISTING file path").create(OPT_EXISTING_FILE)); o.addOption(OptionBuilder.hasArg().withArgName("url").withType(URL.class).withDescription("Specify a URL") .create(OPT_URL)); o.addOption(OptionBuilder.hasArg().withArgName("date").withType(Date.class).withDescription("Specify a date") .create(OPT_DATE)); return o; } static Object getParsedOption(CommandLine cmd, String option) throws ParseException { if (cmd.hasOption(option)) { Object o = cmd.getParsedOptionValue(option); System.out.println("Option " + option + " parsed as " + o + " which is instance of " + o.getClass().getCanonicalName()); return o; } return null; } static Parameters validateParameters(CommandLine cmd) throws ParseException { Parameters p = new Parameters(); Object o = null; Long l = null; Double d = null; Class<?> cls = null; File file = null; FileInputStream fis = null; URL u = null; Date dt = null; /* NUMBERS */ // int o = getParsedOption(cmd, OPT_INT); if (o != null) { l = (Long) o; p.setIntValue(l.intValue()); } // long o = getParsedOption(cmd, OPT_LONG); if (o != null) { l = (Long) o; p.setLongValue(l); } // double o = getParsedOption(cmd, OPT_DOUBLE); if (o != null) { d = (Double) o; p.setDoubleValue(d); } // double o = getParsedOption(cmd, OPT_FLOAT); if (o != null) { d = (Double) o; p.setFloatValue(d.floatValue()); } // class o = getParsedOption(cmd, OPT_CLASS); if (o != null) { cls = (Class<?>) o; p.setClassValue(cls); } // file path (not necessarily found) o = getParsedOption(cmd, OPT_FILE); if (o != null) { file = (File) o; p.setFileValue(file); } // file path (found) o = getParsedOption(cmd, OPT_EXISTING_FILE); if (o != null) { fis = (FileInputStream) o; p.setFis(fis); } // url o = getParsedOption(cmd, OPT_URL); if (o != null) { u = (URL) o; p.setUrl(u); } // url o = getParsedOption(cmd, OPT_DATE); if (o != null) { dt = (Date) o; p.setDate(dt); } // if all goes well returned the parsed parameters return p; } } |
As you can see in the validateParameters()
function there’s no need anymore for Integer.parseInt()
and all those calls — simply call getParsedOptionValue()
— which will either return you the parsed value or throw a ParseException
(based on which you can show the help screen or similar).
What would be cool — and this is more of a request to Apache guys — is to have a way to plug a custom validator and class type into this mechanism, such that you can easily say OK, for class LivsOwnClass.class
use parser and validator LivsOwnClassParser
or similar and still use in your code just a call to getParsedOptionValue()
. This can be done at the moment by doing something like this:
... String s = cmd.getOptionValue("option"); LivsOwnClass loc = new LivsOwnClassParser.parse(s); // this would throw ParseException if wrong value! ... |
However, this mixes the configuration code with the business logic code and it’s not that “cool” at a glance. Hopefully, my voice will be heard and we’ll have some mechanism like this soon?
Anyway, as per usual, attached are all the sources for the examples above: clitypes.tar.bz2