a mighty tiny command line processor

picocli a Mighty Tiny Command Line Interface

Picocli is a one-file framework for creating JVM command line applications (in Java, Groovy, Kotlin, Scala, etc.) with almost zero code. It has an annotations API and a programmatic API, and features usage help with ANSI colors, command line autocompletion and support for nested subcommands. Its source code lives in a single file, so you have the option to include it in source form; this lets end users run picocli-based applications without requiring picocli as an external dependency.

Example

Let’s take a look at an example to see what a picocli-based command line application looks like.

A CheckSum Utility

We will use this small, but realistic, example CheckSum utility to demonstrate various picocli features.

Checksum: an example picocli-based command line application
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Command(description = "Prints the checksum (SHA-1 by default) of a file to STDOUT.",
         name = "checksum", mixinStandardHelpOptions = true, version = "checksum 3.0")
class CheckSum implements Callable<Void> {

    @Parameters(index = "0", description = "The file whose checksum to calculate.")
    private File file;

    @Option(names = {"-a", "--algorithm"}, description = "MD5, SHA-1, SHA-256, ...")
    private String algorithm = "SHA-1";

    public static void main(String[] args) {
        // CheckSum implements Callable, so parsing, error handling and handling user
        // requests for usage help or version help can be done with one line of code.
        CommandLine.call(new CheckSum(), args);
    }

    @Override
    public Void call() throws Exception {
        // your business logic goes here...
        byte[] fileContents = Files.readAllBytes(file.toPath());
        byte[] digest = MessageDigest.getInstance(algorithm).digest(fileContents);
        System.out.printf("%0" + (digest.length*2) + "x%n", new BigInteger(1,digest));
        return null;
    }
}

Given that this program has only 25 lines of code, you may be surprised at how much functionality it packs.

Let’s run this program, first without any input:

Testing the program with invalid input
$ java picocli.example.CheckSum

This gives an error message saying the <file> parameter is missing:

checksum help

The <file> positional parameter does not have a default, so this a mandatory parameter. The --algorithm option does have a default: "SHA-1", so it is optional.

Note that our program does not have any logic to validate the user input. The validation was done automatically by picocli as part of the CommandLine.call(Callable, String[]) invocation in our main method. Later we will show some alternatives that give more control to the application.

Now let’s try running this program with some valid input, and see how the output compares to the GNU md5sum and sha1sum utilities:

Comparing our CheckSum utility against md5sum and sha1sum
$ java picocli.example.CheckSum --algorithm=MD5 picocli-3.9.5.jar
509e3e2602854d5b88e2e7baa476a7fe

$ md5sum picocli-3.9.5.jar
509e3e2602854d5b88e2e7baa476a7fe *picocli-3.9.5.jar


$ java picocli.example.CheckSum picocli-3.9.5.jar
f659a2feef0e8f7f8458aaf7d36c4d92f65320c8

$ sha1sum picocli-3.9.5.jar
f659a2feef0e8f7f8458aaf7d36c4d92f65320c8 *picocli-3.9.5.jar

The hashes are identical. It looks like our checksum utility is working correctly.

You may have noticed that the error message above showed two more options: --help and --version, even though the application does not define any options like @Option(names = {"-h", "--help"}) or similar for version. Where are these coming from?

These two options were added because we defined the command with @Command(mixinStandardHelpOptions = true /* …​ */). Usage help and version information are so common for command line applications that picocli provides this shortcut. Internally this uses a mechanism called "mixins" and we will go into more detail later on how you can define your own mixins to define reusable elements. But first, let’s see what these options actually do, starting with the --version option.

Testing the version option
$ java picocli.example.CheckSum --version
checksum 3.0

The version information shown is what was specified in the command annotation: @Command(version = "checksum 3.0" /* …​ */). We will show later that you can also get the version information from a JAR file’s manifest or some other place. Next, the --help option:

Testing the help option
$ java picocli.example.CheckSum --help
Usage: checksum [-hV] [-a=<algorithm>] <file>
Prints the checksum (SHA-1 by default) of a file to STDOUT.
      <file>      The file whose checksum to calculate.
  -a, --algorithm=<algorithm>
                  MD5, SHA-1, SHA-256, ...
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

The usage help message looks familiar; it is what was shown after the error message "Missing required parameter: <file>" when we gave the program invalid input. The synopsis (after "Usage:") shows the command name as checksum, since that is what we specified in the command definition @Command(name = "checksum" /* …​ */).

To summarize, we just created a full-fledged application with all the bells and whistles expected from a production-quality command line utility, using a minimal amount of code.

What’s Next in this Article

In the rest of this article we will take a quick tour of picocli’s capabilities.

First we will go into some detail of defining commands with options and positional parameters, how picocli converts argument Strings to strongly typed option values, how to define options that can be specified multiple times, including map options like -Dkey=value.

Next, we will show how to customize the usage help and version help, and liven things up with ANSI colors.

Many larger command line applications have subcommands, git being a famous example. We will show how picocli makes it very easy to create commands with subcommands, and how you can use mixins to reuse common options or common command configurations.

After that, we will take a look at picocli’s "entry points": there are methods for just parsing command line parameters and there are "convenience methods" that parse the user input and automatically invoke the business logic of your application.

Furthermore this article will explain how picocli can give your application autocompletion in bash and zsh, how Groovy scripts can use the picocli annotations, and how to build command line applications that integrate with Dependency Injection containers like Spring Boot and Micronaut.

Finally, we will briefly touch on how picocli can be used to create interactive shell applications with the JLine library, and wrap up with a small tutorial for creating native executables with GraalVM to make amazingly fast command line tools.

Defining a Picocli Command

Picocli offers an annotations API and a programmatic API. The programmatic API is useful for dynamically creating commands and command line options on the fly. Typical use cases are domain-specific languages. For example, Groovy’s CliBuilder is implemented using picocli’s programmatic API. Details of the programmatic API are out of scope of this article, but are documented on the project GitHub site. In this article we will focus on the annotations API.

To define a command or a subcommand, annotate a class or a method with @Command. The @Command annotation can be omitted, but is a convenient way to set the command name, description, and other elements of the usage help message. Subcommands can be specified in the @Command annotation but can also be added to a command programmatically.

To define options and positional parameters, annotate a field or a method with @Option or @Parameters. Here is an example of a minimal command:

class Minimal {
    @Option(names = "-x") int x;
}

There is a separate section on subcommands below, but first we will discuss options and positional parameters.

Options and Positional Parameters

Command line arguments can be separated into options and positional parameters. Options have a name, positional parameters are values without a name. Positional parameters often follow the options, but they may be mixed.

Example command with annotated @Option and @Parameters

Picocli has separate annotations for options and positional parameters. The @Option and @Parameters annotations can be used on fields and on methods. Annotated methods can be useful to do validation on single options and positional parameters. In the examples below we will mostly use annotated fields.

Option Names

There are no restrictions on the option name prefix: applications are free to create Windows DOS-style options like /A /B, Unix POSIX-style short options like -x -y, GNU-style long options like --long-option or anything else. You can also use all styles together for a single option if you want. An option can have as many names as you want.

Picocli does have special support for POSIX-style short options, in the sense that the parser recognizes clustered short options. For example, given the following command definition:

Example command with single-character POSIX-style options
@Command(name = "tar")
class Tar {
    @Option(names = "-x") boolean extract;
    @Option(names = "-v") boolean verbose;
    @Option(names = "-f") File file;
}

Picocli will consider the following two inputs equivalent to each other:

Multiple short options can be specified separately or grouped together after a single '-' delimiter
tar -xvfTARFILE
tar -x -v -f TARFILE

Default Values

As we already saw earlier with the CheckSum example in the beginning of this article, an easy way to give an option or positional parameter a default value is to assign the annotated field a value in its declaration. The initial value becomes the default value:

Defining a default value in the field declaration
@Option(names = "-x")
double multiplier = Double.PI;

@Parameters
File file = new File(System.getProperty("user.home"));

Both the @Option and the @Parameters annotations also have a defaultValue attribute where a default value can be specified. This is especially useful for annotated methods. For example:

Defining a default value using annotations
@Option(names = "-x", defaultValue = "123", paramLabel = "MULTIPLIER",
        description = "The multiplier, ${DEFAULT-VALUE} by default.")
void setMultiplier(int multiplier) { this.multiplier = multiplier; }

@Parameters(defaultValue = ".", paramLabel = "DIRECTORY",
            description = "The directory to write to, '${DEFAULT-VALUE}' by default.")
void setDirectory(File directory) { this.directory = directory; }

Two things to note: the description may contain a ${DEFAULT-VALUE} variable that will be replaced with the option’s default value in the usage help message. Also, use the paramLabel to specify the name of the option parameter or positional parameter in the usage help. For example:

Showing default values in the usage help message with ${DEFAULT-VALUE} variables
     DIRECTORY   The directory to write to, '.' by default.
 -x=MULTIPLIER   The multiplier, 123 by default.

An alternative is to implement the IDefaultProvider interface, for example to get defaults from a properties file. The interface looks like the below.

The IDefaultProvider interface for externalizing default values
public interface IDefaultValueProvider {
    String defaultValue(ArgSpec argSpec) throws Exception;
}
The ArgSpec class is part of the programmatic API and is the superclass of OptionSpec and PositionalParamSpec.

The default provider can be wired into the command via the @Command annotation:

Using a custom default provider
@Command(defaultProvider = MyDefaultProvider.class)
class MyCommand { /*...*/ }

Password Options

For options and positional parameters marked as interactive, the user is prompted to enter a value on the console. When running on Java 6 or higher, the user input is not echoed to the console.

Example usage:

Example command with an interactive password option
class Login implements Callable<Object> {
    @Option(names = {"-u", "--user"}, description = "User name")
    String user;

    @Option(names={"-p", "--passphrase"}, interactive=true, description="Passphrase")
    String password;

    public Object call() throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(password.getBytes());
        System.out.printf("Hi %s, your passphrase is hashed to %s.%n", user,
                Base64.getEncoder().encodeToString(md.digest()));
        return null;
    }
}

When this command is invoked like this:

CommandLine.call(new Login(), "-u", "user123", "-p");

Then the user will be prompted to enter a value:

Enter value for --passphrase (Passphrase):

After the user enters a password value and presses enter, the call() method is invoked, which prints something like the following:

Hi user123, your passphrase is hashed to 75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8=.

Positional Parameters

Any command line arguments that are not subcommands or options (or option parameters) are interpreted as positional parameters.

Use the (zero-based) index attribute to specify exactly which parameters to capture. Omitting the index attribute means the field captures all positional parameters. Array or collection fields can capture multiple values.

The index attribute accepts range values, so an annotation like @Parameters(index="2..4") captures the arguments at index 2, 3 and 4. Range values can be open-ended. For example, @Parameters(index="3..*") captures all arguments from index 3 and up.

For example:

Positional parameters can be defined with and without the index attribute
class PositionalParameters {
    @Parameters(hidden = true)  // "hidden": don't show this param in usage help
    List<String> allParameters; // no "index" attribute: captures _all_ arguments

    @Parameters(index = "0")    InetAddress host;
    @Parameters(index = "1")    int port;
    @Parameters(index = "2..*") File[] files;
}

Type Conversion

When arguments are matched on the command line, the text value is converted to the type of the option or positional parameter. For annotated fields this is the type of the field.

Out of the box, picocli supports many common types: all primitive types and their wrapper types, any enum type, BigDecimal, BigInteger, File, Path, URL, URI, InetAddress, java.util.regex.Pattern, Date, Time, Timestamp, all value objects in Java 8’s java.time package, and more. See the user manual for the full list.

If necessary, applications can customize and extend this by defining their own type converters. The ITypeConverter interface looks like this:

Custom type converters need to implement the ITypeConverter interface
public interface ITypeConverter<K> {
    K convert(String value) throws Exception;
}

Custom type converters can be registered with the CommandLine::registerConverter(Class, ITypeConverter) method. All options and positional parameters with the specified type will be converted by the specified converter. For example:

Registering custom type converters
CommandLine cmd = new CommandLine(app)
cmd.registerConverter(Locale.class, s -> new Locale.Builder().setLanguageTag(s).build());
cmd.registerConverter(Cipher.class, s -> Cipher.getInstance(s));

Type converters can also be registered for specific options and positional parameters:

Example of registering a custom type converter for a single option
class App {
    @Option(names = "--sqlType", converter = SqlTypeConverter.class)
    int sqlType;
}

class SqlTypeConverter implements ITypeConverter<Integer> {
    public Integer convert(String value) throws Exception {
        switch (value) {
            case "ARRAY"  : return Types.ARRAY;
            case "BIGINT" : return Types.BIGINT;
            ...
        }
    }
}

Multiple Values

Multiple parameters, or multiple occurrences of an option can be captured in an array, Map or Collection field. The elements can be of any type for which a converter is registered. For example:

Defining options and positional parameters that can be specified multiple times on the command line
import java.util.regex.Pattern;
import java.io.File;

class Convert {
    @Option(names = "-patterns", description = "the regex patterns to use")
    Pattern[] patterns;

    @Parameters(/* type = File.class, */ description = "the files to convert")
    List<File> files; // picocli infers type from the generic type

    @Option(names = "-D") // support -Dkey=value properties
    Map<String, String> properties;

    @Option(names = {"-u", "--timeUnit"})
    Map<TimeUnit, Long> timeout; // picocli infers type from the generic type
}
Example input and expected result for multi-value options and positional parameters
String[] args = { "-patterns", "a*b", "-patterns", "[a-e][i-u]",
                  "file1.txt", "file2.txt",
                  "-uDAYS=3", "-u", "HOURS=23", "-u=MINUTES=59",
                  "--timeUnit=SECONDS=13", "-Dkey=value" };
Convert convert = CommandLine.populateCommand(new Convert(), args);

// convert.patterns now has two Pattern objects
// convert.files now has two File objects
// convert.timeout now has four {TimeUnit:Long} key-value pairs

Split Regex

Options and parameters may specify a split regular expression used to split each option parameter into smaller substrings. Each of these substrings is converted to the type of the collection or array.

Example of an option with a split regex
@Option(names = "-option", split = ",")
int[] values;

A single command line argument like the following will be split up and three int values are added to the array:

-option 111,222,333

Dynamic Version Information

Remember that the CheckSum example showed version information from a static @Command(version = "xxx") attribute? Often you want to manage version information in a single place, and have picocli obtain this information dynamically at runtime. For example, an implementation may return version information obtained from the JAR manifest, a properties file or some other source.

The @Command annotation supports a versionProvider attribute, where applications can specify a IVersionProvider implementation class:

Example command with a custom version provider
@Command(versionProvider = my.custom.VersionProvider.class)
class App { ... }

Custom version providers need to implement the picocli.CommandLine.IVersionProvider interface:

The IVersionProvider interface for externalizing version information
public interface IVersionProvider {
    String[] getVersion() throws Exception;
}

See the VersionProviderDemo classes in the picocli-examples module on GitHub for examples of getting the version from the JAR manifest file or a version properties file.

Usage Help

We have already seen some of the annotation attributes that can be used to customize aspects of the usage help message. For example, the @Command(name = "xxx") to set the command name, the paramLabel attribute to set the name of the option parameter or positional parameter, and the ${DEFAULT-VALUE} variable in the description of options or positional parameters.

There is also a ${COMPLETION-CANDIDATES} variable that can be used in the description of an option or positional parameter that will be expanded into the values of an enum, or the completionCandidates of a non-enum option.

Below follow a few more annotation attributes for customizing the usage help message.

Usage Width

The default width of the usage help message is 80 characters. This can be modified with the @Command(usageHelpWidth = <int>) attribute. End users can override with system property picocli.usage.width.

Section Headings

Section headings can be used to make usage message layout appear more spacious. The example below demonstrates the use of embedded line separator (%n) format specifiers:

Using annotation attributes to customize the usage help message
@Command(name = "commit",
        sortOptions = false,
        headerHeading = "Usage:%n%n",
        synopsisHeading = "%n",
        descriptionHeading = "%nDescription:%n%n",
        parameterListHeading = "%nParameters:%n",
        optionListHeading = "%nOptions:%n",
        header = "Record changes to the repository.",
        description = "Stores the current contents of the index in a new commit " +
                "along with a log message from the user describing the changes.")
class GitCommit { ... }

The below example demonstrates what a customized usage message with more vertical spacing and custom headings can look like.

Screenshot of usage help with Ansi codes enabled

Option Ordering

By default, options are sorted alphabetically. You can switch this off by setting @Command(sortOptions = false). This will show options in the order they are declared in the class. You can explicitly specify the order in which they should be listed with the @Option(order = <int>) attribute.

Abbreviated Synopsis

If a command is very complex and has many options, it is sometimes desirable to suppress details from the synopsis with the @Command(abbreviateSynopsis = true) attribute. An abbreviated synopsis looks something like this:

Example abbreviated synopsis
Usage: <main class> [OPTIONS] [<files>...]

Note that the positional parameters are not abbreviated.

Custom Synopsis

For even more control of the synopsis, use the customSynopsis attribute to specify one or more synopsis lines. For example:

Example custom synopsis with multiple lines
Usage: ln [OPTION]... [-T] TARGET LINK_NAME   (1st form)
  or:  ln [OPTION]... TARGET                  (2nd form)
  or:  ln [OPTION]... TARGET... DIRECTORY     (3rd form)
  or:  ln [OPTION]... -t DIRECTORY TARGET...  (4th form)

To produce a synopsis like the above, specify the literal text in the customSynopsis attribute:

Using the customSynopsis attribute to define a multi-line custom synopsis
@Command(synopsisHeading = "", customSynopsis = {
        "Usage: ln [OPTION]... [-T] TARGET LINK_NAME   (1st form)",
        "  or:  ln [OPTION]... TARGET                  (2nd form)",
        "  or:  ln [OPTION]... TARGET... DIRECTORY     (3rd form)",
        "  or:  ln [OPTION]... -t DIRECTORY TARGET...  (4th form)",
})
class Ln { ... }

Hidden

The @Command, @Option and @Parameters annotations all have a hidden attribute. Setting this attribute to true means the subcommand, option or parameters won’t be shown in the usage help message.

ANSI Colors

Picocli generates help that uses ANSI styles and colors for contrast to emphasize important information like commands, options, and parameters. The default color scheme for these elements can be overridden programmatically and with system properties.

In addition, you can use colors and styles in the descriptions, header and footer of the usage help message.

Picocli supports a custom markup notation for mixing colors and styles in text, where @| starts a styled section, and |@ ends it. Immediately following the @| is a comma-separated list of colors and styles, so @|STYLE1[,STYLE2]…​ text|@. For example:

Using markup notation for embedding colors and styles in usage help text
@Command(description = "Custom @|bold,underline styles|@ and @|fg(red) colors|@.")

Description with Ansi styles and colors

Adding a banner is easy. The usage help is the face of your application, so be creative!

Using the command header to define a banner
@Command(header = {
                "@|green        .__                    .__  .__ |@",
                "@|green ______ |__| ____  ____   ____ |  | |__||@",
                "@|green \\____ \\|  |/ ___\\/  _ \\_/ ___\\|  | |  ||@",
                "@|green |  |_> >  \\  \\__(  <_> )  \\___|  |_|  ||@",
                "@|green |   __/|__|\\___  >____/ \\___  >____/__||@",
                "@|green |__|           \\/           \\/         |@",
                ""}, // ...

picocli.Demo

Subcommands

When your application grows larger, it often makes sense to organize pieces of functionality into subcommands. Git, Angular, Docker, and Puppet are examples of applications that make good use of subcommands.

Picocli has extensive support for subcommands: subcommands are easy to create, can have multiple aliases, and can be nested to any level.

Subcommands can be registered declaratively with the @Command annotation’s subcommands attribute, like this:

Defining subcommands declaratively with annotations
@Command(subcommands = {
    GitStatus.class,
    GitCommit.class,
    GitAdd.class // ...
})
public class Git { ... }

Alternatively, subcommands can be registered programmatically with the CommandLine.addSubcommand method, like this:

Defining subcommands programmatically with addSubcommand
CommandLine commandLine = new CommandLine(new Git())
        .addSubcommand("status",   new GitStatus())
        .addSubcommand("commit",   new GitCommit())
        .addSubcommand("add",      new GitAdd());

A third, more compact, way to register subcommands is to have a @Command class with @Command-annotated methods. The methods are automatically registered as subcommands of the @Command class. For example:

By default, @Command-annotated methods are registered as subcommands of the enclosing @Command class
@Command(name = "git", resourceBundle = "Git_Messages")
class Git {
    @Option(names = "--git-dir", descriptionKey = "GITDIR") // description from bundle
    Path path;

    @Command
    void commit(@Option(names = {"-m", "--message"}) String commitMessage,
                @Parameters(paramLabel = "<file>")   File[] files) {
        // ... implement business logic
    }
}
With @Command methods it may be useful to put the option and parameters descriptions in a resource bundle to avoid cluttering the code.

Mixins for Reuse

You may find yourself defining the same options, parameters or command attributes in many command line applications. To reduce duplication, picocli supports both subclassing and mixins as ways to reuse such options and attributes. In this section we will focus on mixins.

For example, let’s say that we want to reuse some usage help attributes that give a spacious layout, and a verbosity option. We create a ReusableOptions class, like this:

Example of some attributes and an option we want to reuse in multiple commands
@Command(synopsisHeading      = "%nUsage:%n%n",
         descriptionHeading   = "%nDescription:%n%n",
         parameterListHeading = "%nParameters:%n%n",
         optionListHeading    = "%nOptions:%n%n",
         commandListHeading   = "%nCommands:%n%n")
public class ReusableOptions {

    @Option(names = { "-v", "--verbose" }, description = {
        "Specify multiple -v options to increase verbosity.",
        "For example, `-v -v -v` or `-vvv`" })
    protected boolean[] verbosity = new boolean[0];
}

A command can include a mixin by annotating a field with @Mixin. All picocli annotations found in the mixin class are added to the command that has a field annotated with @Mixin. The following example shows how we would mix in the sample ReusableOptions class defined above:

Using the @Mixin annotation to apply reusable attributes to a command
@Command(name = "zip", description = "Example reuse with @Mixin annotation.")
public class MyCommand {

    // adds the options defined in ReusableOptions to this command
    @Mixin
    private ReusableOptions myMixin;
    ...
}

This adds the -v option to the zip command. After parsing, the results can be obtained from the annotated fields as usual:

Inspecting the parse result of a command with a mixin
MyCommand zip = CommandLine.populateCommand(new MyCommand(), "-vvv");

// the options defined in ReusableOptions have been added to the zip command
assert zip.myMixin.verbosity.length == 3;

Parsing and Running a Picocli Application

The general outline of any command line application is:

  • define the top-level command and its subcommands

  • define options and positional parameters

  • parse the user input

  • inspect the result

The previous sections explained how to define commands with options and positional parameters. For reference, the diagram below gives a high-level overview of the classes and interfaces involved in defining commands.

Classes and Interfaces for Defining a CommandSpec Model

Classes and Interfaces for Defining a CommandSpec Model

In the following sections we discuss parsing and running picocli applications. In our examples we will use the minimal command that we saw earlier:

class Minimal {
    @Option(names = "-x") int x;
}

For the next step, parsing the user input, there are broadly two approaches: either just parse the input, or parse the input and run the business logic.

Simply Parsing

The static method CommandLine::populateCommand accepts a command object and an array of command line arguments. It parses the input, injects values for matched options and positional parameters into the annotated elements of the command, and returns the command object. For example:

Using the populateCommand method for simple use cases
String[] args = new String[] {"-x", "5"};
Minimal result = CommandLine.populateCommand(new Minimal(), args);
assert result.x == 5;

The populateCommand static method is useful for very straightforward commands and for testing, but is limited. To customize the parser behaviour you need to create a CommandLine instance and call the parseArgs method:

Using the parseArgs method for more flexibility
Minimal minimal = new Minimal();
CommandLine cmd = new CommandLine(minimal)
    .setUnmatchedArgumentsAllowed(true); // configure parser to accept unknown args

cmd.parseArgs("-x", "5", "-y=unknown");
assert minimal.x == 5;
assert cmd.getUnmatchedArguments().equals(Arrays.asList("-y=unknown"));

Parsing and Running

The above examples are a bit academic. A real-world application needs to be more robust, specifically:

  1. Handle invalid user input, and report any problems to the user (potentially suggesting alternative options and subcommands for simple typos if we want to get fancy).

  2. Check if the user requested usage help, and print this help and abort processing if this was the case.

  3. Check if the user requested version information, and print this information and abort processing if this was the case.

  4. Finally, run the business logic of the application.

Classes Related to Parsing Command Line Arguments

Classes Related to Parsing Command Line Arguments

The above is so common that picocli provides some shortcuts, the so-called "convenience" methods. These methods take care of invalid user input and requests for usage help and version information as described above.

  • CommandLine static methods run, call and invoke accept a Runnable, Callable or a @Command-annotated Method object. Any subcommands constructed from the annotations must also be @Command-annotated Methods or classes implementing Runnable or Callable. After the input was parsed successfully, the Runnable, Callable or Method for the subcommand specified by the end user on the command line is invoked.

  • CommandLine instance methods parseWithHandler and parseWithHandlers calls the specified result handler when parsing succeeds, or the exception handler when an error occurred. The run, call and invoke static methods delegate to this method with the default result handler (RunLast) and default exception handler.

The default result handler (RunLast) takes care of requests for usage help and version information as described above, and invokes the most specific subcommand. The default exception handler takes care of invalid user input and runtime errors in the business logic.

The static run, call and invoke methods are simple and straightforward but are limited in that they won’t allow you to customize the parser behaviour or the usage help message. The parseWithHandler methods are more verbose but more flexible. For example:

Using the parseWithHandler method for more flexibility
class MyRunnable implements Runnable {
    @Option(names = "-x") int x;

    public void run() {
        System.out.println("You specified " + x);
    }
}
CommandLine cmd = new CommandLine(new MyRunnable())
    .setCommandName("myRunnable")        // customize usage help message
    .setUnmatchedArgumentsAllowed(true); // customize parser behaviour

cmd.parseWithHandler(new RunLast(), "-x", "5");

Inspecting the Parse Result

After parsing, the application needs to inspect the specified options and positional parameters to determine what action to take. When using the annotations API, the most straightforward thing to do is to inspect the value of the fields annotated with @Option and @Parameters.

When options and positional parameters were defined programmatically instead of with the annotations API, the alternative is to inspect the ParseResult object returned by the CommandLine::parseArgs method.

Via the ParseResult class the application can determine whether an option or positional parameter was specified on the command line, what its value was, whether the user requested usage help or version info, whether a subcommand was specified, whether any undefined options were specified, and more. For example:

Using the ParseResult class for inspecting the parse results
CommandLine cmd = new CommandLine(new Minimal());
ParseResult parseResult = cmd.parseArgs("-x", "5");

int defaultValue = -1;
assert parseResult.hasMatchedOption("-x");
assert parseResult.matchedOptionValue("-x", defaultValue) == 5;

Autocompletion

Picocli-based applications can have command line completion in Bash or ZSH Unix shells. Picocli can generate an autocompletion script tailored to your application.

With this script installed, users can type the first few letters of a subcommand or an option, then press the TAB key, and the Unix shell will complete the subcommand or option.

In the case of multiple possible completions, the Unix shell will display all subcommands or options beginning with those few characters. The user can type more characters and press TAB again to see a new, narrowed-down list if the typed characters are still ambiguous, or else complete the subcommand or option.

Generating a Completion Script

First, we need to create a starter script to run our command line application. The name of this script will be the name of our command.

In this example we will use the CheckSum application from the beginning of this article. Let’s say we want to call our command checksum, so we create a starter script called checksum, with the following contents:

Contents of the checksum starter script
#!/usr/bin/env bash

LIBS=/home/user/me/libs
CP="${LIBS}/checksum.jar:${LIBS}/picocli-3.9.5.jar"
java -cp "${CP}" 'picocli.example.CheckSum' $@

You probably want to chmod 755 checksum to make the script executable. Try calling it on the command line with ./checksum --version to see if the script works.

Next, we generate the completion script for our checksum command. To do this, we invoke picocli.AutoComplete, and give it the name of the class and the name of the command:

Generating a completion script
$ java -cp "checksum.jar:picocli-3.9.5.jar" picocli.AutoComplete -n checksum picocli.example.CheckSum

This will generate a file called checksum_completion in the current directory.

Installing the Completion Script

Simply source the completion script to install it in your current bash session:

Installing the completion script in the current session
$ . checksum_completion

Now, if you type checksum [TAB] the bash shell will show the available options for this command.

To install the completion script permanently, add it to your .bash_profile. Below is a one-liner that adds all completion scripts in the current directory to your .bash_profile. It will not create duplicate entries, so it can be invoked multiple times.

Installing the completion script more permanently in your .bash_profile
$ for f in $(find . -name "*_completion"); do line=". $(pwd)/$f"; grep "$line" ~/.bash_profile || echo "$line" >> ~/.bash_profile; done

Completion Candidates

Other than options and subcommands, picocli can deduce completion candidates for parameters of certain types. For example, File, Path, InetAddress and enum types allow picocli to generate completion candidates from the current directory, your /etc/hosts file, and the enum values, respectively.

Additionally, you can specify completionCandidates for an option. For example, in the CheckSum application, we can get completion for the --algorithms option parameter by defining the option as follows:

Defining completionCandidates for an option to allow autocompletion on option parameters
private static class AlgoList extends ArrayList<String> {
    AlgoList() { super(Arrays.asList("MD5", "SHA-1", "SHA-256")); }
}
@Option(names = {"-a", "--algorithm"}, completionCandidates = AlgoList.class,
        description = "${COMPLETION-CANDIDATES}, ...")
private String algorithm = "SHA-1";

Values in the completionCandidates list are shown as completion candidates when the user presses [TAB] after the -a option, similarly to enum typed options.

Groovy Scripts

Picocli offers special support for Groovy scripts, to allow the picocli annotations to be used directly in the script without creating a class. All that is needed is to add the @picocli.groovy.PicocliScript annotation to the script. For example:

Using picocli annotations in a Groovy script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Grab('info.picocli:picocli:3.9.5')
@Command(header = [
 $/@|green    ___                            ___ _           _                  |@/$,
 $/@|green   / __|_ _ ___  _____ ___  _     / __| |_  ___ __| |__ ____  _ _ __  |@/$,
 $/@|green  | (_ | '_/ _ \/ _ \ V / || |   | (__| ' \/ -_) _| / /(_-< || | '  \ |@/$,
 $/@|green   \___|_| \___/\___/\_/ \_, |    \___|_||_\___\__|_\_\/__/\_,_|_|_|_||@/$,
 $/@|green                         |__/                                         |@/$
 ],
    description = "Print a checksum of each specified FILE.",
    mixinStandardHelpOptions = true, version = 'checksum v1.2.3',
    footerHeading = "%nFor more details, see:%n", showDefaultValues = true,
    footer = [
      "[1] https://docs.oracle.com/javase/9/docs/specs/security/standard-names.html",
      "ASCII Art thanks to http://patorjk.com/software/taag/" ]
)
@picocli.groovy.PicocliScript
import groovy.transform.Field
import java.security.MessageDigest
import static picocli.CommandLine.*

@Parameters(arity = "1", paramLabel = "FILE",
            description = "The file(s) whose checksum to calculate.")
@Field private File[] files

@Option(names = ["-a", "--algorithm"], description = [
        "MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512, or",
        "  any other MessageDigest algorithm. See [1] for more details."])
@Field private String algorithm = "SHA-1"

files.each {
  println ""+MessageDigest.getInstance(algorithm).digest(it.bytes).encodeHex()+"\t"+it
}

The usage help message for our script looks like this:

Customized header and footer with styles and colors

Dependency Injection

Spring Boot

When your command is annotated with @org.springframework.stereotype.Component, Spring can autodetect it for dependency injection. The below example shows how to use picocli with Spring Boot:

Entry point of an application using picocli with Spring Boot
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import picocli.CommandLine;

@SpringBootApplication
public class MySpringBootApp implements CommandLineRunner {

    @Autowired
    private MyCommand myCommand;

    public static void main(String[] args) {
        // let Spring instantiate and inject dependencies
        SpringApplication.run(MySpringBootApp.class, args);
    }

    @Override
    public void run(String... args) {
        // let picocli parse command line args and run the business logic
        CommandLine.call(myCommand, args);
    }
}

The business logic of your command looks like any other picocli command with options and parameters.

Example picocli command using services injected by Spring Boot
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;

@Component
@Command(name = "myCommand")
public class MyCommand implements Callable<Void> {

    @Autowired
    private SomeService someService;

    @Option(names = { "-x", "--option" }, description = "example option")
    private boolean flag;

    public Void call() throws Exception {
        // business logic here
        someService.doUsefullStuff(flag);
        return null;
    }
}

Micronaut

Micronaut is an up-and-coming star in the world of microservices, and has strong dependency injection capabilities. It minimizes runtime reflection and instead uses annotation processing at compile time, resulting in very fast startup time and reduced memory footprint.

Micronaut offers special support for using picocli to create standalone command-line applications that use and interact with services in a Microservice infrastructure with its PicocliRunner class. You may be interested to know that the Micronaut CLI itself is also implemented using picocli under the hood to support its subcommands like mn create-app, mn create-function, etc.

Example picocli command with a PicocliRunner entry point, using services injected by Micronaut
import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.RxHttpClient;
import javax.inject.Inject;

import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Command(name = "myMicronautApp")
public class MyMicronautApp implements Runnable {

    @Client("https://api.github.com")
    @Inject RxHttpClient client;

    @Option(names = {"-x", "--option"}, description = "example option")
    boolean flag;

    public static void main(String[] args) {
        // let Micronaut instantiate and inject services
        PicocliRunner.run(MyMicronautApp.class, args);
    }

    public void run() {
        // business logic here
    }
}

Interactive Shell Applications

JLine is a well-known library for creating interactive shell applications.

From the JLine web site: "It is similar in functionality to BSD editline and GNU readline but with additional features that bring it on par with the ZSH line editor."

JLine and picocli complement each other well. JLine has support for history, highlighting, input tokenization, and a framework for command line completion. Picocli can parse an array of strings and execute a command or subcommand.

Combining these two libraries makes it easy to build powerful interactive shell applications. Picocli has two modules, picocli-shell-jline2 and picocli-shell-jline3, for this purpose.

These modules have a PicocliJLineCompleter class that shows context-sensitive completion candidates for options, option parameters and subcommands of a set of picocli commands. The readme of the modules have examples. Applications that use picocli to define their commands no longer need to hand-code JLine Completers for their commands and options. (An early version of this is used in the Micronaut CLI.)

Blazingly Fast with GraalVM

picocli on graalvm

GraalVM allows you to compile your programs ahead-of-time into a native executable. The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM. This is especially useful for command line utilities, which are often short-lived.

GraalVM has limited support for Java reflection and it needs to know ahead of time the reflectively accessed program elements.

The picocli-codegen module includes a ReflectionConfigGenerator tool that generates a GraalVM configuration file. This configuration file lists the program elements that will be accessed reflectively in a picocli-based application. This configuration file should be passed to the -H:ReflectionConfigurationFiles=/path/to/reflectconfig option of the native-image GraalVM utility.

Generating the Configuration File

Using the ReflectionConfigGenerator tool to generate a reflection configuration file for GraalVM
java -cp \
picocli-3.9.5.jar:picocli-codegen-3.9.5.jar:checksum.jar \
picocli.codegen.aot.graalvm.ReflectionConfigGenerator picocli.example.CheckSum > reflect.json

The generated reflect.json files looks something like this:

Partial contents of a generated reflection configuration file
[
  {
    "name" : "picocli.example.CheckSum",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
        { "name" : "algorithm" },
        { "name" : "file" },
    ],
  },
...
]

Creating a Native Image

We create a native image for our application with the following command:

Creating a native executable for our application
graalvm-ce-1.0.0-rc12/bin/native-image \
    -cp picocli-3.9.5.jar:checksum.jar:jansi-1.17.1.jar \
    -H:ReflectionConfigurationFiles=reflect.json \
    -H:+ReportUnsupportedElementsAtRuntime \
    -H:Name=checksum \
    --static --no-server picocli.example.CheckSum

The reflect.json is in the current directory, and I added -H:+ReportUnsupportedElementsAtRuntime to get a useful error message in case something goes wrong.

Running the Native Image

If compilation went well, we now have a native executable checksum in the current directory.

To compare the difference in startup speed, compare running it in HotSpot versus the native executable.

Running the command in HotSpot
$ time java -cp "picocli-3.9.5.jar;checksum.jar" \
    picocli.example.CheckSum picocli-3.9.5.jar
509e3e2602854d5b88e2e7baa476a7fe

real    0m0.517s
user    0m0.869s
sys     0m0.082s

On Oracle Hotspot, it takes about half a second to start the JVM and print the checksum. Now, we run the native image:

Running the native image
$ time ./checksum picocli-3.9.5.jar
509e3e2602854d5b88e2e7baa476a7fe

real    0m0.006s
user    0m0.003s
sys     0m0.002s

The execution time is now down to 6 milliseconds!

All command line parsing functionality works as expected, with type conversion, validation and help with ANSI colors. This is exciting news when you want to write command line applications and services in Java and have them run instantaneously.

Conclusion

Picocli has many more features you may be interested in, like resource bundles, @-files, parser configuration options, the @ParentCommand annotation, the @Spec annotation, the programmatic API, and more…​ I hope I’ve been able to give you some idea of picocli’s capabilities, and where it could be useful. Star the project on GitHub if you like it!