Tuesday, December 27, 2011

Args4j vs JCommander for Parsing Command Line Parameters

In the past, I've always used Apache Commons CLI for parsing command line options passed to programs and have found it quite tedious because of all the boiler plate code involved. Just take a look at their Ant Example and you will see how much code is required to create each option.

As an alternative, there are two annotation-based command line parsing frameworks which I have been evaluating recently:

I'm going to use the Ant example to illustrate how to parse command line options using these two libraries. I'm only going to use a few options in my example because they are all very similar. Here is an extract of the help output for Ant, which I will be aiming to replicate:
ant [options] [target [target2 [target3] ...]]
Options:
  -help, -h              print this message
  -lib <path>            specifies a path to search for jars and classes
  -buildfile <file>      use given buildfile
    -file    <file>              ''
    -f       <file>              ''
  -D<property>=<value>   use value for given property
  -nice  number          A niceness value for the main thread:
Args4j (v2.0.12)
The class below demonstrates how to parse command line options for Ant using Args4j. The main method parses some sample arguments and also prints out the usage of the command.
import static org.junit.Assert.*;
import java.io.File;
import java.util.*;
import org.kohsuke.args4j.*;

/**
 * Example of using Args4j for parsing
 * Ant command line options
 */
public class AntOptsArgs4j {

  @Argument(metaVar = "[target [target2 [target3] ...]]", usage = "targets")
  private List<String> targets = new ArrayList<String>();

  @Option(name = "-h", aliases = "-help", usage = "print this message")
  private boolean help = false;

  @Option(name = "-lib", metaVar = "<path>",
          usage = "specifies a path to search for jars and classes")
  private String lib;

  @Option(name = "-f", aliases = { "-file", "-buildfile" }, metaVar = "<file>",
          usage = "use given buildfile")
  private File buildFile;

  @Option(name = "-nice", metaVar = "number",
          usage = "A niceness value for the main thread:\n"
          + "1 (lowest) to 10 (highest); 5 is the default")
  private int nice = 5;

  private Map<String, String> properties = new HashMap<String, String>();
  @Option(name = "-D", metaVar = "<property>=<value>",
          usage = "use value for given property")
  private void setProperty(final String property) throws CmdLineException {
    String[] arr = property.split("=");
    if(arr.length != 2) {
        throw new CmdLineException("Properties must be specified in the form:"+
                                   "<property>=<value>");
    }
    properties.put(arr[0], arr[1]);
  }

  public static void main(String[] args) throws CmdLineException {
    final String[] argv = { "-D", "key=value", "-f", "build.xml",
                            "-D", "key2=value2", "clean", "install" };
    final AntOptsArgs4j options = new AntOptsArgs4j();
    final CmdLineParser parser = new CmdLineParser(options);
    parser.parseArgument(argv);

    // print usage
    parser.setUsageWidth(Integer.MAX_VALUE);
    parser.printUsage(System.err);

    // check the options have been set correctly
    assertEquals("build.xml", options.buildFile.getName());
    assertEquals(2, options.targets.size());
    assertEquals(2, options.properties.size());
  }
}
Running this program prints:
 [target [target2 [target3] ...]] : targets
 -D <property>=<value>            : use value for given property
 -f (-file, -buildfile) <file>    : use given buildfile
 -h (-help)                       : print this message
 -lib <path>                      : specifies a path to search for jars and classes
 -nice number                     : A niceness value for the main thread:
                                    1 (lowest) to 10 (highest); 5 is the default
JCommander (v1.13)
Similarly, here is a class which demonstrates how to parse command line options for Ant using JCommander.
import static org.junit.Assert.*;
import java.io.File;
import java.util.*;
import com.beust.jcommander.*;

/**
 * Example of using JCommander for parsing
 * Ant command line options
 */
public class AntOptsJCmdr {

  @Parameter(description = "targets")
  private List<String> targets = new ArrayList<String>();

  @Parameter(names = { "-help", "-h" }, description = "print this message")
  private boolean help = false;

  @Parameter(names = { "-lib" },
             description = "specifies a path to search for jars and classes")
  private String lib;

  @Parameter(names = { "-buildfile", "-file", "-f" },
             description = "use given buildfile")
  private File buildFile;

  @Parameter(names = "-nice", description = "A niceness value for the main thread:\n"
        + "1 (lowest) to 10 (highest); 5 is the default")
  private int nice = 5;

  @Parameter(names = { "-D" }, description = "use value for given property")
  private List<String> properties = new ArrayList<String>();

  public static void main(String[] args) {
    final String[] argv = { "-D", "key=value", "-f", "build.xml",
                            "-D", "key2=value2", "clean", "install" };
    final AntOptsJCmdr options = new AntOptsJCmdr();
    final JCommander jcmdr = new JCommander(options, argv);

    // print usage
    jcmdr.setProgramName("ant");
    jcmdr.usage();

    // check the options have been set correctly
    assertEquals("build.xml", options.buildFile.getName());
    assertEquals(2, options.targets.size());
    assertEquals(2, options.properties.size());
  }
}
Running this program prints:
Usage: ant [options]
 targets
  Options:
    -D                      use value for given property
                            Default: [key=value, key2=value2]
    -buildfile, -file, -f   use given buildfile
    -help, -h               print this message
                            Default: false
    -lib                    specifies a path to search for jars and classes
    -nice                   A niceness value for the main thread:
1 (lowest) to
                            10 (highest); 5 is the default
                            Default: 5
Args4j vs JCommander
As you can see from the implementations above, both frameworks are very similar. There are a few differences though:
  1. JCommander does not have an equivalent to Arg4j's metaVar which allows you to display the value that an option might take. For example, if you have an option called "-f" which takes a file, you can set metaVar="<file>" and Args4j will display -f <file> when it prints the usage. This is not possible in JCommander, so it is difficult to see which options take values and which ones don't.

  2. JCommander's @Parameter option can only be applied to fields, not methods. This makes it slightly restrictive. In Args4j, you can add the annotation on a "setter" method, which allows you to tweak the value before it is set. In JCommander, you would have to create a custom converter.

  3. In the example above, JCommander was unable to place -D property=value options into a map. It was able to save them into a list and then you would have to do some post-processing to convert the elements in the list to key-value pairs in a map. On the other hand, Args4j was able to put the properties straight into the map by applying the annotation on a setter method.

  4. JCommander's usage output is not as pretty as Args4j's. In particular, the description of the "nice" option is not aligned correctly.

Based purely on this example, the winner is Args4j. However, note that there are other features present in JCommander which are not available in Args4j, such as parameter validation and password type parameters. Please read the documentation to find out which one is better suited to your needs. But one thing is quite clear: annotation based command line parsing is the way forward!

5 comments:

  1. JCommander has a built-in way to do the -Dkey=value pattern: @DynamicParameter. It must be on a field of type Map. You can customize the assignment character (defaults to "=").

    Agree on the lack of metaVar though, that's killing me.

    ReplyDelete
  2. I don't think that `@DynamicParameter` has been released yet because it is not in the latest version (1.20). Good to know that it is coming up though!

    ReplyDelete
  3. Thanks for the handy comparison!

    I think #2 (@Parameter on a setter method) has been implemented in JCommander recently: https://groups.google.com/d/topic/jcommander/EchNx4rJFGE/discussion

    I've been using args4j for a while, but it's got one shortcoming that annoys me: boolean options are always flags; they can't take a parameter. So you can't have a boolean option which defaults to true, where the user says: 'mycommand --flag=false' to turn the flag off. (You can work around it by using String instead of boolean, but then you have to convert String->boolean in the setter method.)

    ReplyDelete
    Replies
    1. Correction, Args4j can have boolean options with params, but you have to use a different OptionHandler:

      http://java.net/jira/browse/ARGS4J-12?focusedCommentId=350653&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_350653

      Delete
  4. I want to know about jcommander.. so any one can help me in writing a simple program on jcommander that how to parse argument from command line.i want to use two commands like 1-Run & 2- List command.

    ReplyDelete