CLI package (in djutils-ext)

Introduction

Most code needs an executable program for which parameters can be set, e.g.:

java -jar FederateStarter.jar --port=5000 --server=local

Parsing these command line parameters is quite some work. Several packages have been developed to parse the Command Line Interface (CLI) in an easy manner. In DJUTILS we use and extend picocli (see https://picocli.info). Picocli is an open-source one-file framework that can parse all sorts of command line parameters with very little code. The package has an Apache license and can be freely used. It is embedded into DJUTILS-EXT through Maven.

Including DJUTILS-EXT

When a POM-file is created for the project, the following snippet needs to be included to include DJUTILS-EXT:

<dependencies>
  <dependency>
    <groupId>org.djutils</groupId>
    <artifactId>djutils-ext</artifactId>
    <version>1.02.00</version>
  </dependency>
</dependencies>

The CLI utilities have been put in a separate project because they are dependent on DJUNITS and on picoli.

Usage in DJUTILS

The class CliIUtil offers a helper method to display --help and --version without starting the program. The method is used as follows:

 public static void main(final String[] args) throws Exception
 {
     Program program = new Program(); // initialize the Checkable class with the @Option information
     CliUtil.execute(program, args); // register Unit converters, parse the command line, catch --help, --version and error
     // do rest of what the main method should do
 }

When the program is Checkable, the check() method is called after the arguments have been parsed. Here, further checks on the arguments (i.e., range checks) can be carried out. Potentially, check() can also provide other initialization of the program to be executed, but this can better be provided by other methods in main() . Make sure that expensive initialization is not carried out in the constructor of the program class that is given to the execute method. Alternatively, move the command line options to a separate class, e.g. called Options and initialize that class rather than the real program class. The real program can then take the values of the program from the Options class. An example:

public class Program
 {
     @Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, version = "1.0")
     public static class Options implements Checkable
     {
         @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "80")
         private int port;

         public int getPort()
         {
             return this.port;
         }

         @Override
         public void check() throws Exception
         {
             if (this.port <= 0 || this.port > 65535)
                 throw new Exception("Port should be between 1 and 65535");
         }
     }

     public Program()
     {
         // initialization for the program; avoid really starting things
     }

     public static void main(final String[] args)
     {
         Options options = new Options();
         CliUtil.execute(options, args);
         System.out.println("port = " + options.getPort());
         // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
     }
 }

DJUNITS Type Converters

The class CliUnitConverters offers conversion methods for DJUNITS scalars so these can be used on the command line, e.g.:

    java -jar ProgramApp.jar --timeout=5min

The option that uses the Duration converter is specified as follows:

    /** how much time max before timeout kicks in? */
    @Option(names = {"-t", "--timeout"}, description = "Maximum duration before timeout stops the process",
            defaultValue = "10min")
    private Duration timeout;

Converters have to be registered before they can be used. When the CliUtil.execute(program, args); method is used, all coverters for DJUNITS scalars are registered. In case you want to register an individual converter, call the folllowing code on the CommandLine cmd:

    cmd.registerConverter(Duration.class, new DURATION());

Building a converter works as follows: make a method in the converter class that implements a convert() method from String to the intended data type. See below for an example.

    /**
     * Convert a duration String with unit on the command line to a Duration scalar.
     */
    public static class DURATION implements ITypeConverter<Duration>
    {
        /** {@inheritDoc} */
        @Override
        public Duration convert(final String value) throws Exception
        {
            return Duration.valueOf(value);
        }
    }

All converters for Scalar double DJUTILS classes have already been coded in the class CliUnitConverters.

Changing default options

When a subclass inherits CLI options from a superclass, sometimes default values of options (and version numbers, program description, or program name) need to be changed. Several methods help to make these changes of the values in the annotations. The code below uses the Options settings in the Program and makes changes to them:

public static void main(final String[] args)
{
    Options options = new Options();
    // change the default port value from 80 to 8080
    CliUtil.changeOptionDefault(options, "port", "8080"); 
    // change the version of the file to the last changed date of the Options class file
    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    CliUtil.changeCommandVersion(options, formatter.format(
         new Date(ClassUtil.classFileDescriptor(options.getClass()).getLastChangedDate())));
    CliUtil.changeCommandName(options, "NewProgram");
    CliUtil.changeCommandDescription(options, "New version of the program");
    CliUtil.execute(options, args);
    System.out.println("port = " + options.getPort());
    // you can now call methods on the program, e.g. for real initialization using the CLI parameters in options
}

An example of the output when running the program without options is:

port = 8080

An example of the output of this file with options "--version" is:

2019-08-14 00:25:42

An example of the output of this file with options "--help" is:

Usage: NewProgram [-hV] [-p=<port>]
New version of the program
  -h, --help          Show this help message and exit.
  -p, --port=<port>   Internet port to use
  -V, --version       Print version information and exit.

It is clear that the default values have been changed.

Note that the actual values of the annotation properties have been changed through reflection on the class level. This means that future instances of the Options in the same run will have changed values as well. As we often use the CLI options only once, and at the start of the public static void main, this is usually not a problem. When it is, it might be better to create a new Program / Options class that inherits from the previous one and has different values for the @Command properties. When a default value of an @Option property needs to be changed, calling changeOptionDefault is the only way to reflect this in the options for the user.