Core Java

Migrating from Commons CLI to picocli

Apache Commons CLI, initially released in 2002, is perhaps the most widely used java command line parser, but its API shows its age. Applications looking for a modern approach with a minimum of boilerplate code may be interested in picocli. Why is it worth the trouble to migrate, and how do you migrate your Commons CLI-based application to picocli? Picocli offers a fluent API with strong typing, usage help with ANSI colors, autocompletion and a host of other features. Let’s take a look using Checkstyle as an example.

Why Migrate?

Is migrating from Commons CLI to picocli worth the trouble? What is the benefit of moving from one command line parser to another? Is this more than just redecorating the living room of our application?

End User Experience

Commons CLI

What are the benefits for end users?

Command line completion. Picocli-based applications can have command line completion in bash and zsh shells, as well as in JLine-based interactive shell applications.

Beautiful, highly readable usage help messages. The usage help generated by Commons CLI is a bit minimalistic. Out of the box, picocli generates help that uses ANSI styles and colors for contrast to emphasize important information like commands, options, and parameters. The help message layout is easy to customize using the annotations. Additionally, there is a Help API in case you want something different. See the picocli README for some example screenshots.

Support for very large command lines via @-files, or “argument files”. Sometimes users need to specify command lines that are longer than supported by the operating system or the shell. When picocli encounters an argument beginning with the character @, it expands the contents of that file into the argument list. This allows applications to handle command lines of arbitrary length.

Developer Experience

Commons CLI

What are the benefits for you as developer?

Generally a picocli application will have a lot less code than the Commons CLI equivalent. The picocli annotations allow applications to define options and positional parameters in a declarative way where all information is in one place. Also, picocli offers a number of conveniences like type conversion and automatic help that take care of some mechanics so the application can focus more on the business logic. The rest of this article will show this in more detail.

Documentation: picocli has an extensive user manual and detailed javadoc.

Troubleshooting. Picocli has a built-in tracing facility to facilitate troubleshooting. End users can use system property picocli.trace to control the trace level. Supported levels are OFF, WARN, INFO, and DEBUG. The default trace level is WARN.

Future Expansion

Commons CLI

Finally, other than the immediate pay-off, are there any future benefits to be gained by migrating from Commons CLI to picocli?

Picocli has a lot of advanced features. Your application may not use these features yet, but if you want to expand your application in the future, picocli has support for nested subcommands (and sub-subcommands to any depth), mixins for reuse, can easily integrate with Dependency Injection containers, and a growing tool framework to generate source code, documentation and configuration files from a picocli CommandSpec model.

Finally, picocli is actively maintained, whereas Commons CLI seems to be near-dormant with 6 releases in 16 years.

An Example Migration: CheckStyle

Commons CLI

A command line application needs to do three things:

  1. Define the supported options
  2. Parse the command line arguments
  3. Process the results

Let’s compare how this is done in Commons CLI and in picocli, using CheckStyle’s com.puppycrawl.tools.checkstyle.Main command line utility as an example.

The full source code before and after the migration is on GitHub.

Defining Options and Positional Parameters

Defining Options with Commons CLI

Commons CLI has multiple ways to define options: Options.addOption, constructing a new Options(…​) and invoking methods on this object, the deprecated OptionBuilder class, and the recommended Option.Builder class.

The Checkstyle Main class uses the Options.addOption method. It starts by defining a number of constants for the option names:

/** Name for the option 's'. */
private static final String OPTION_S_NAME = "s";

/** Name for the option 't'. */
private static final String OPTION_T_NAME = "t";

/** Name for the option '--tree'. */
private static final String OPTION_TREE_NAME = "tree";

... // and more. Checkstyle Main has 26 options in total.

The Main.buildOptions method uses these constants to construct and return a Commons CLI Options object that defines the supported options:

private static Options buildOptions() {
    final Options options = new Options();
    options.addOption(OPTION_C_NAME, true, "Sets the check configuration file to use.");
    options.addOption(OPTION_O_NAME, true, "Sets the output file. Defaults to stdout");
    ...
    options.addOption(OPTION_V_NAME, false, "Print product version and exit");
    options.addOption(OPTION_T_NAME, OPTION_TREE_NAME, false,
            "Print Abstract Syntax Tree(AST) of the file");
    ...
    return options;
}

Defining Options with Picocli

In picocli you can define supported options either programmatically with builders, similar to the Commons CLI approach, or declaratively with annotations.

Picocli’s programmatic API may be useful for dynamic applications where not all options are known in advance. If you’re interested in the programmatic approach, take a look at the CommandSpec, OptionSpec and PositionalParamSpec classes. See also Programmatic API for more detail.

In this article we will use the picocli annotations. For the CheckStyle example, this would look something like the below:

@Option(names = "-c", description = "Sets the check configuration file to use.")
private File configurationFile;

@Option(names = "-o", description = "Sets the output file. Defaults to stdout")
private File outputFile;

@Option(names = "-v", versionHelp = true, description = "Print product version and exit")
private boolean versionHelpRequested;

@Option(names = {"-t", "--tree"}, description = "Print Abstract Syntax Tree(AST) of the file")
private boolean printAST;

Comparison

Declarative

Commons CLI

With Commons CLI, you build a specification by calling a method with String values. One drawback of an API like this is that good style compels client code to define constants to avoid “magic values”, like the Checkstyle Main class dutifully does.

With picocli, all information is in one place. Annotations only accept String literals, so definition and usage are automatically placed together without the need to declare constants. This results in cleaner and less code.

Strongly Typed

Commons CLI

Commons CLI uses a boolean flag to denote whether the option takes an argument or not.

Picocli lets you use types directly. Based on the type, picocli “knows” how many arguments the option needs: boolean fields don’t have an argument, Collection, Map and array fields can have zero to any number of arguments, and any other type means the options takes a single argument. This can be customized (see arity) but most of the time the default is good enough.

Picocli encourages you to use enum types for options or positional parameters with a limited set of valid values. Not only will picocli validate the input for you, you can also show all values in the usage help message with @Option(description = "Valid values: ${COMPLETION-CANDIDATES}"). Enums also allow command line completion to suggest completion candidates for the values of the option.

Less Code

Commons CLI

Picocli converts the option parameter String value to the field type. Not only does it save the application from doing this work, it also provides some minimal validation on the user input. If the conversion fails, a ParameterException is thrown with a user-friendly error message.

Let’s look at an example to see how useful this is. The Checkstyle Main class defines a -x, --exclude-regexp option that allows uses to specify a number of regular expressions for directories to exclude.

With Commons CLI, you need to convert the String values that were matched on the command line to java.util.regex.Pattern objects in the application:

/**
 * Gets the list of exclusions from the parse results.
 * @param commandLine object representing the result of parsing the command line
 * @return List of exclusion patterns.
 */
private static List<Pattern> getExclusions(CommandLine commandLine) {
    final List<Pattern> result = new ArrayList<>();

    if (commandLine.hasOption(OPTION_X_NAME)) {
        for (String value : commandLine.getOptionValues(OPTION_X_NAME)) {
            result.add(Pattern.compile(value));
        }
    }
    return result;
}

By contract, in picocli you would simply declare the option on a List<Pattern> (or a Pattern[] array) field. Since picocli has a built-in converter for java.util.regex.Pattern, all that is needed is to declare the option. The conversion code goes away completely. Picocli will instantiate and populate the list if one or more -x options are specified on the command line.

/** Option that allows users to specify a regex of paths to exclude. */
@Option(names = {"-x", "--exclude-regexp"},
        description = "Regular expression of directory to exclude from CheckStyle")
private List<Pattern> excludeRegex;

Option Names

Commons CLI

Commons CLI supports “short” and “long” options, like -t and --tree. This is not always what you want.

Picocli lets an option have any number of names, with any prefix. For example, this would be perfectly fine in picocli:

@Option(names = {"-cp", "-classpath", "--class-path"})

Positional Parameters

Commons CLI

In Commons CLI you cannot define positional parameters up front. Instead, its CommandLine parse result class has a method getArgs that returns the positional parameters as an array of Strings. The Checkstyle Main class uses this to create the list of File objects to process.

In picocli, positional parameters are first-class citizens, like named options. Not only can they be strongly typed, parameters at different positions can have different types, and each will have a separate entry and description listed in the usage help message.

For example, the Checkstyle Main class needs a list of files to process, so we declare a field and annotate it with @Parameters. The arity = "1..*" attribute means that at least one file must be specified, or picocli will show an error message about the missing argument.

@Parameters(paramLabel = "file", arity = "1..*", description = "The files to process")
private List<File> filesToProcess;

Help Options

Commons CLI

It is surprisingly difficult in Commons CLI to create an application with a required option that also has a --help option. Commons CLI has no special treatment for help options and will complain about the missing required option when the user specifies <command> --help.

Picocli has built-in support for common (and custom) help options.

Parsing the Command Line Arguments

Commons CLI

Commons CLI has a CommandLineParser interface with a parse method that returns a CommandLine representing the parse result. The application then calls CommandLine.hasOption(String) to see if a flag was set, or CommandLine.getOptionValue(String) to get the option value.

Picocli populates the annotated fields as it parses the command line arguments. Picocli’s parse…​ methods also return a ParseResult that can be queried on what options were specified and what value they had, but most applications don’t actually need to use the ParseResult class since they can simply inspect the value that were injected into the annotated fields during parsing.

Processing the Results

Commons CLI
Business concept isolated on white

When the parser is done, the application needs to run its business logic, but first there are some things to check:

  • Was version info or usage help requested? If so, print out the requested information and quit.
  • Was the user input invalid? Print out an error message with the details, print the usage help message and quit.
  • Finally run the business logic – and deal with errors thrown by the business logic.

With Commons CLI, this looks something like this:

int exitStatus;
try {
    CommandLine commandLine = new DefaultParser().parse(buildOptions(), args);

    if (commandLine.hasOption(OPTION_VERSION)) { // --version
        System.out.println("Checkstyle version: " + version());
        exitStatus = 0;
    } else if (commandLine.hasOption(OPTION_HELP)) { // --help
        printUsage(System.out);
        exitStatus = 0;
    } else {
        exitStatus = runBusinessLogic(); // business logic
    }
} catch (ParseException pex) { // invalid input
    exitStatus = EXIT_WITH_CLI_VIOLATION;
    System.err.println(pex.getMessage());
    printUsage(System.err);
} catch (CheckstyleException ex) { // business logic exception
    exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
    ex.printStackTrace();
}
System.exit(exitStatus);

Picocli offers some convenience methods that take care of most of the above. By making your command implement Runnable or Callable, the application can focus on the business logic. At its simplest, this can look something like this:

public class Main implements Callable<Integer> {
    public static void main(String[] args) {
        CommandLine.call(new Main(), args);
    }

    public Integer call() throws CheckstyleException {
        // business logic here
    }
}

The Checkstyle Main class needs to control the exit code, and has some strict internal requirements for error handling, so we ended up not using the convenience methods and kept the parse result processing very similar to what it was with Commons CLI. Improving this area is on the picocli todo list.

Usage Help Message

Picocli uses ANSI colors and styles in the usage help message on supported platforms. This doesn’t just look good, it also reduces the cognitive load on the user: the contrast make the important information like commands, options, and parameters stand out from the surrounding text.

Applications can also use ANSI colors and styles in the description or other sections of the usage help message with a simple markup like @|bg(red) text with red background|@. See the relevant section of the user manual.

For CheckStyle, we kept it to the bare minimum, and the resulting output for CheckStyle looks like this:

Commons CLI

Wrapping Up: a Final Tip

Be aware that the Commons CLI default parser will recognize both single hyphen (-) and double hyphen (--) long options, even though the usage help message will only show options with double hyphens. You need to decide whether to continue to support this.

In picocli you can use @Option(names = "-xxx", hidden = true) to declare long options with a single hyphen if you want to mimic the exact same behaviour as Commons CLI: hidden options in picocli are not shown in the usage help message.

Conclusion

Migrating from Commons CLI to picocli can give end users a better user experience, and can give developers significant benefits in increased maintainability and potential for future expansion. Migration is a manual process, but is relatively straightforward.

Update: the CheckStyle project accepted a pull request with the changes in this article. From CheckStyle 8.15 its command line tools will use picocli. It looks like the CheckStyle maintainers were happy with the result:

Checkstyle migrated from Apache CLI to @picocli (will be released in 8.15), finally documentation of CLI arguments is now well organized in declarative way in code, and checkstyle’s CLI is following CLI best practices.

— CheckStyle maintainer Roman Ivanov

Published on Java Code Geeks with permission by Remko Popma, partner at our JCG program. See the original article here: Migrating from Commons CLI to picocli

Opinions expressed by Java Code Geeks contributors are their own.

Remko Popma

Remko is Algo Team Leader at SMBC Nikko Securities, developing automated trading systems for Japanese equities. In open source, he works on Log4j2 performance improvements and the picocli library.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button