options - a tutorial

Parsing options

For the sake of this tutorial, let us assume that we want to write a drop-in replacement for rsync in Java. Consider a simple command line interface:

rsync -a --rsh=ssh ./here/ remote.example.com:/srv/there

Windwards options makes the option parsing part of this task easy, by using an annotation called @Option.

public class RsyncCommand {
  @Option(shortName = "a")
  public boolean archive = false;
}

This declares an option called "--archive" also giving it the short name "-a". Windwards options always use the full name of the variable as the so called long-option name.

Long options are preceeded by two dashes ("--"). Short names are always one character long. You can know that an option is a short option because it has only one dash ("-") in front of it. This is known as the GNU style of options. Option --archive (or -a for short) does not take an argument. Such options are represented by variables of type boolean in Windwards options. If the option is present, it will be set to true, otherwise it will default to false.

Next option in our example command line above is "--rsh" (short name "-e"). Option "--rsh" does require an argument, the name of an rsh-compatible binary. In order to capture this, we can use a String option, like so:

public class RsyncCommand {
  @Option(shortName = "a")
  private boolean archive = false;
  @Option(shortName = "e")
  private String rsh = "rsh";
}

Here we initialize the rsh member to "rsh", the default binary to use for remote file transfers unless the option is used. Because rsh is of type String, Windwards options will require a value for this argument. Values are supplied like so "--rsh=ssh".

RsyncCommands needs to actually do something. Implementing this is outside the scope of this tutorial, but let's assume there is a method called activate, like so:

public class RsyncCommand {
  @Option(shortName = "a")
  private boolean archive = false;
  @Option(shortName = "e")

  private String rsh = "rsh";  public void activate() {
    System.out.println("Archive mode is set to " + 
        (this.archive ? "on" : "off") + " and rsh is " + this.rsh);
    // Actually copying files goes here
  }
}

Data types and defaults

Windwards options makes assumptions about annotated options based on their declared data type. The assumptions can be summarized in a number of conventions:

  • Convention 1: If you assign a value to an option, that value is the default value
  • Convention 2: All non-boolean options will require a value
  • Convention 3: Collections can receive the same option multiple times

Convention 1 is really straight forward: if the parsed string does not contain option X, the matching field will not be touched.

Convention 2 means that Windwards options does not implement the notion of an optional value, that is, you cannot create an option that can do either "-e" or "-e <value>". Either, an option is boolean and cannot accept an argument ("-a") or it is any other type and it requires a value ("-e <rsh-binary>"). There is one exception. Consider the following:

@Option
public Boolean tristate = null;

A Boolean with capital B can be initialized to null, which will create a tri-state option. In this special case, you can do "--tristate=on" which will set it to true, or "--tristate=off" which will result in false. Not giving the option will leave the value at null, which might be interpreted as "don't care", "guess" or perhaps "auto".

The final convention is particularily useful:

@Option
public List<String> exclude = new LinkedList<String>();

This allows option "--exclude=<pattern>" to be given multiple times in order to instruct our rsync replacement to exclude som set of files when copying.

A collection need to be initialized to an instance, or Windwards options will not be able to work with it, as it can't easily know which collection is expected.

Parsing arguments

But what about the rest of the rsync command line?

These are called arguments. In order to handle them, we need to implement the net.windwards.options.AcceptsArguments interface.

public class RsyncCommand implements AcceptsArguments {
  ...

  private List<String> fileArgs = new LinkedList<String>();

  public boolean handleArgument(String arg) {
    fileArgs.add(arg);
    return true;
  }
}

The parser will call the handleArgument method with each argument, until the method returns false.

For simple use cases, simply adding all the arguments to a list and act on them later makes sense. In our rsync example, the last argument is the destination to sync to and all the preceeding arguments are sources.

Why could you ever want to return false? Well, there are some use cases where the rest of the command line is meant for "someone else". Consider the following example command line:

ssh -l quest remote.example.com rm /tmp/remove-this

If we needed to parse this command line, everything that comes after the hostname is actually a command to be executed on the remote host. Once your handleArgument() method found a hostname, it would return false. Any remaining arguments would be returned by the parse() method. We might have a main method that looked something like this:

public static void main(String[] args) throws Exception {
  SSHClient ssh = new SSHClient();
  OptionParser<SSHClient> parser = new OptionParser<SSHClient>(ssh);
  String[] remoteCmd = parser.parse(args);
  ssh.connect();
  if (remoteCmd.length == 0) {
    ssh.interactive();
  else {
    ssh.execute(remoteCmd);
  }
}

... or something to that effect.

Applying the parser

Now we have all we need to show how option parsing works. Normally, the command line is passed to a Java program by the java runtime via the arguments passed to the static main method of the starting class.

In order to actually parse a command line using RsyncCommand, a simple main method may look like this:

public static void main(String[] args) throws Exception {
  RsyncCommand command = new RsyncCommand();
  OptionParser<RsyncCommand> parser = new OptionParser<RsyncCommand>(command);
  parser.parse(args);
  command.activate();
}

First, we create an instance of the object that is to receive options from the command line. This can be any object with @Option annotations on it. Then we create an instance of net.windwards.options.OptionParser and give it the annotated object. This will scan the object and find all fields annotated with @Option.

Finally, we parse an actual command line. This will update the various fields in the command object handed to the parser's constructor with the options given in the args String array.

Wrapping it up

Executing a java command from command line can be daunting. In order to actually get a nice command line tool, you need to create a simple shell script that wraps the command, so that it becomes convenient. On a Linux platform, you may want to cut-and-paste the following into a shell script that you stick in /usr/local/bin (or wherever):

#!/bin/sh

JAVA=/usr/lib/jvm/jdk1.7.0
JAR=/usr/local/lib/javarsync.jar

$JAVA -cp $JAR com.example.javarsync.RsyncCommand $*

If the above shell script is called /usr/local/bin/javarsync, in your shell you should now be able to do this:

$ javarsync -a ./here ./there

Don't forget to set execute bit on javarsync.