Using Apache Commons CLI to Parse Arguments

Posted by & filed under , .

computer codeI’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 to withType() means that the parser will try to first parse the value as a Long (and return an instace of java.lang.Long). Failing that, it will then attempt to parse it as a Double — and if that fails too the getParsedOptionValue() method will throw a ParseException. There is no way to get an Integer or Float back so your code has to convert from long to int or double to float.
  • Using a File.class doesn’t really do much apart from returning a File 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 use FileInputStream in withType() in order to validate that the file exists (which is what the source comment suggests). Instead you get still an instance of File 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 (supports http://, ftp://) — however, as to be expected really, it does not verify that the domain exists. So you will get an instance of URL 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 invoke getParsedOptionValue() for an option which was built via withType(Date.class) throws an UnsupportedOperationException. 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