Apache Commons CLI Tutorial

Last Updated on

Time needed: 30 minutes

In this tutorial I'll show how to use an Apache Commons CLI library in order to create a console application that accepts various user-provided options, validates them, and prints a help message if something goes wrong.

We'll create a simple tool, called cli-random, that will generate pseudo-random numbers based solely on input parameters. We don't want to have any user interaction. It's going to be a Maven project, that will use the newest version of the library, which is 1.4 at the time of writing of this tutorial.

General information

Apache Commons CLI might seem very very basic, as it lacks any advanced features available in other libraries. But on the other hand it's rather straightforward to use, it's well tested, lightweight, and has no external dependencies. There isn't any "magic" happened under the hood, so you're unlikely to be surprised by its behavior.

Apache Commons CLI supports several types of options, such as:

  • Single letter options that do not take any argument. For example, on Unix-like systems a "rm -rf [file]" command removes files, -r does it recursively and -f doesn't ask for confirmations.
  • Long options, such as --tail=100. The aforementioned "rm -rf" can be rewritten as "rm --recursive --force".
  • Long options with single hyphen, like "java -version"
  • Java-like options that are passed as system properties. For example the option -Duri=localhost that can be retrieved with System.getProperty("uri") inside Java application.
  • Short options with values.

In our application we'll use a solution that's very popular on Unix-like systems, in which options can be written using both short (-r) and long (--recursive) conventions.

An application that leverages Commons CLI follow this pattern:

  1. Create a set of rules describing expected arguments.
  2. Parse the user-supplied arguments using the given rules.
  3. Retrieve and use the parsed information.

The minimal application

I've already mentioned that our console application will generate pseudo-random numbers, but let's define what exactly we will be working on.

  • The application will run in batch mode, without waiting for user input. It will accept parameters that will define the produced output. Output will consist of numbers, each one in a separate line.
  • It will accept two mandatory options
    • --min that will require a value and will define the lower bound (inclusive) of the number generation range
    • --max that will require a value and will define the upper bound (inclusive) of the number generation range
  • It will accept three non-mandatory options
    • --seed or -s that will take a numerical seed as a value
    • --numbers or -n that will specify how many numbers will be returned, defaults to 1
    • --class or -c that will define what object will be used as a pseudo-random generator

Some examples:

java cli-random --min 0 --max 10 -n 5

Will produce 5 numbers between 0 and 10, both inclusive.

java cli-random --min 5 --max 20 -c java.security.SecureRandom -s 123

Will produce 1 number, greater or equal that 5 and lower or equal that 20, using the SecureRandom object, and 123 as a seed.

I guess it's easy enough, so let's start. We'll begin from a very basic application that will be gradually developed and improved.

Parsing the options

We'll start off by creating a Maven project that depends on the library our project will be based on. The pom.xml is shown below.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dth</groupId>
    <artifactId>cli-random</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.4</version>
        </dependency>
    </dependencies>
</project>

Note that no version was specified. By the time of writing this article, the maven plugin bundled with my IDE expected the code to be compatible with Java 6. Because the application will be very simple, and we're not going to use any new language constructs, I opted to leave it unchanged.

Then we'll create the class that will get the options, parse them, and return a generated pseudo-random number. We'll start off with max and min. Here it is.

package com.dth;

import org.apache.commons.cli.*;

import java.util.Random;

public class CliRandom {

    public static void main(String[] args) throws ParseException {
        CommandLineParser parser = new DefaultParser();
        CommandLine commandLine = parser.parse(prepareOptions(), args);

        int min = Integer.parseInt(commandLine.getOptionValue("min"));
        int max = Integer.parseInt(commandLine.getOptionValue("max"));

        Random r = new Random();
        System.out.println(Math.round(r.nextInt(max)) + min);
    }

    private static Options prepareOptions() {
        Options options = new Options();

        // Required options
        Option minOption = Option.builder().required()
              .longOpt("min")
              .hasArg()
              .build();

        Option maxOption = Option.builder().required()
              .longOpt("max")
              .hasArg()
              .build();

        options.addOption(minOption)
                .addOption(maxOption);

        return options;
    }
}

The main method is pretty straightforward: we're parsing the arguments using the DefaultParser.

The library also provides some deprecated parsers, like GnuParser or PosixParser. Don't use them, the DefaultParser will handle all the conventions mentioned earlier.

We parse the values using a list of Option objects and we retrieve the parsed values from the CommandLine.

Options object

Option is, in fact, the most interesting class. Options defines what arguments we expect and specifies their details. I think it should be pretty obvious what the builder does.

Also, please do not use the constructors, because they are incredibly misleading. Compare the builder present in the example above, with the following code:

Option option = new Option(null, "min", true, null);
option.setRequired(true);

The first null is the short option. But because we don't want them for min and max, we'll leave it empty. Well, if someone didn't pay attention to the parameters, the same code would assign "min" as the short notation, and there would be no long notation.

Option option = new Option("min", true, null);

Builder is also more concise, because there's no need to invoke methods setting the fields not present in the constructor, such as setRequired().

CLI parsing errors

Our program crashes when it finds an unrecognized option. Which is, in our example, rather fine, because we don't want it to keep working. The error message tells exactly what's wrong:

Exception in thread "main" org.apache.commons.cli.UnrecognizedOptionException: Unrecognized option: -x

If we wanted to ignore the unrecognized parameters, we'd have to set one more parameter in the parse method, which would look like that:

parser.parse(prepareOptions(), args, true)

The last argument tells the parser not to throw an exception, when an unrecognized argument is found, but it will stop parsing anyway. So, if an unrecognized argument is the first one, we won't get the min and max parameters, even if they were correct. We still couldn't continue our work. Because of this limitation there's no point in using it in our scenario.

Typed parameters

So far in our code we've been manually parsing the parameters, because we know that min and max are supposed to be integers. But there's a slightly better way to do that. Commons CLI can already do it for us. All we need to do is to define the types in our Option object:

Option minOption = Option.builder().required()
      .longOpt("min")
      .type(Number.class)
      .hasArg()
      .build();

Why a Number and not Long or Integer, for example? Because, unfortunately, it will not work. The parser has only few predefined types. The min and max no longer need to be parsed, but we'll have to cast them to a Number and get a value.

int min = ((Number) commandLine.getParsedOptionValue("min")).intValue();

Not very useful when it comes to numbers, in fact the only difference is that it will always throw a ParseException, while previously it could throw a NumberFormatException when we parsed the integer ourselves.

We can, however, do something interesting if we define the type as Object. In that case, an argument will cause and Object to be instantiated, of a class that was passed as an argument parameter.

Option classOption = Option.builder("c")
      .longOpt("class")
      .type(Object.class)
      .hasArg()
      .build();

Because there's no getParsedOptionValue method with a default value, we'll have to do a null check. The argument is optional, after all.

Random random = ((Random) commandLine.getParsedOptionValue("c"));
if (random == null) {
    random = new Random();
}

Now, if someone invokes the application in the following way, a SecureRandom will be used instead. Nice, isn't it?

java cli-random --min 0 --max 500 -class java.security.SecureRandom

Notice that I used the -class instead of --class. It doesn't matter, the DefaultParser we're using will handle all of them.

Help page

Apache CLI can also automatically generate help messages, that will be shown when someone makes any mistake when running our application. It's very useful, as we can't realistically expect anyone to remember all the possible options. With exceptions we only print an error message if one of the required parameters is missing, but there's no mention about the optional ones.

It's very easy to change it, though. We'll add descriptions to our parameters.

Option minOption = Option.builder().required().desc("minimum number (inclusive)").build();
      .longOpt("min")
      .type(Number.class)
      .hasArg()

And instead of throwing an exception, we'll print a message generated by a HelpFormatter object.

new HelpFormatter().printHelp("cli-random", options);

Complete application

We're almost finished. We'll add the remaining parameters, the number of results and the seed. We'll refactor it a bit and fix some issues, like the additional next line after each execution.

package com.dth;

import org.apache.commons.cli.*;

import java.util.Random;

public class CliRandom {

    private static final String MIN = "min";
    private static final String MAX = "max";
    private static final String RANDOM_CLASS = "c";
    private static final String SEED = "s";
    private static final String RESULTS = "n";

    public static void main(String[] args) {
        CommandLineParser parser = new DefaultParser();
        Options options = prepareOptions();

        try {
            CommandLine commandLine = parser.parse(prepareOptions(), args);

            // Getting required arguments
            int min = ((Number) commandLine.getParsedOptionValue(MIN)).intValue();
            int max = ((Number) commandLine.getParsedOptionValue(MAX)).intValue();

            // Getting optional arguments
            Random random = ((Random) commandLine.getParsedOptionValue(RANDOM_CLASS));
            if (random == null) {
                random = new Random();
            }

            if (commandLine.hasOption(SEED)) {
                long seed = ((Number) commandLine.getParsedOptionValue(SEED)).longValue();
                random.setSeed(seed);
            }

            long numbers = 1;
            if (commandLine.hasOption(RESULTS)) {
                numbers = ((Number) commandLine.getParsedOptionValue(RESULTS)).longValue();
            }

            for (int i = 0; i < numbers; i++) {
                System.out.print(Math.round(random.nextInt(max)) + min);
                if (i != numbers - 1) {
                    System.out.println();
                }
            }
        } catch (ParseException ex) {
            System.out.println(ex.getMessage());
            new HelpFormatter().printHelp("cli-random", options);
        }
    }

    private static Options prepareOptions() {
        Options options = new Options();

        options.addOption(getMinOption())
                .addOption(getMaxOption())
                .addOption(getRandomClassOption())
                .addOption(getSeedOption())
                .addOption(getNumberOfResultsOption());

        return options;
    }

    private static Option getMinOption() {
        return Option.builder().required().desc("minimum number (inclusive)")
                .longOpt(MIN)
                .type(Number.class)
                .hasArg()
                .build();
    }

    private static Option getMaxOption() {
        return Option.builder().required().desc("maximum number (inclusive)")
                .longOpt(MAX)
                .type(Number.class)
                .hasArg()
                .build();
    }

    private static Option getRandomClassOption() {
        return Option.builder(RANDOM_CLASS).desc("class extending Random, that will provide" +
                " random numbers, for example: java.security.SecureRandom")
                .longOpt("class")
                .type(Object.class)
                .hasArg()
                .build();
    }

    private static Option getNumberOfResultsOption() {
        return Option.builder(RESULTS).desc("number of results, 1 by default")
                .longOpt("numbers")
                .type(Number.class)
                .hasArg()
                .build();
    }

    private static Option getSeedOption() {
        return Option.builder(SEED).desc("seed, by default determined by" +
                " Random implementation")
                .longOpt("seed")
                .type(Number.class)
                .hasArg()
                .build();
    }
}

And here's the example of an automatically generated help message:

Missing argument for option: s
usage: cli-random
-c,--class class extending Random, that will provide random
numbers, for example: java.security.SecureRandom
--max maximum number (inclusive)
--min minimum number (inclusive)
-n,--numbers number of results, 1 by default
-s,--seed seed, by default determined by Random implementation

It can be further improved by splitting the code into classes, to make it reusable and more robust. Or by writing unit tests to check if everything works as expected, which will require changing the System.out to our own stream in tests. I'll leave the rest to you.

You can find the complete project here, on GitHub.

Leave a Reply