Skip to content
Remko Popma edited this page Mar 18, 2018 · 45 revisions

Programmatic API

Tip
For most applications the annotations API is a better fit than the programmatic API: the annotation syntax is more compact, easier to read, and easier to maintain. See this introductory article and for more details, the user manual.

Picocli 3.0 offers a programmatic API for creating command line applications, in addition to annotations. The programmatic API allows applications to dynamically create command line options on the fly, and also makes it possible to create idiomatic domain-specific languages for processing command line arguments, using picocli, in other JVM languages.

1. Example

CommandSpec spec = CommandSpec.create();
spec.mixinStandardHelpOptions(true); // usageHelp and versionHelp options
spec.addOption(OptionSpec.builder("-c", "--count")
        .paramLabel("COUNT")
        .type(int.class)
        .description("number of times to execute").build());
spec.addPositional(PositionalParamSpec.builder()
        .paramLabel("FILES")
        .type(List.class)
        .auxiliaryTypes(File.class) // List<File>
        .description("The files to process").build());
CommandLine commandLine = new CommandLine(spec);

commandLine.parseWithSimpleHandlers(new AbstractSimpleParseResultHandler() {
    public void handle(ParseResult pr) {
        int count = pr.optionValue('c', 1);
        List<File> files = pr.positionalValue(0, Collections.<File>emptyList());
        for (int i = 0; i < count; i++) {
            for (File f : files) {
                System.out.printf("%d: %s%n", i, f);
            }
        }
    }
}, args);

2. Configuration

The following classes are the main model classes used to configure the parser:

  • CommandSpec

  • OptionSpec

  • PositionalParamSpec

2.1. CommandSpec

2.1.1. Command Name and Version

CommandSpec models a command. It has a name and a version, both of which may be empty. For example:

CommandSpec cmd = CommandSpec.create()
    .name("mycommand")
    .version("My Command v1.0");

It also has a UsageMessageSpec to configure aspects of the usage help message.

2.1.2. Usage Help

cmd.usageMessage()
        .headerHeading("Header heading%n")
        .header("header line 1", "header line 2")
        .descriptionHeading("Description heading%n")
        .description("description line 1", "description line 2")
        .optionListHeading("Options%n")
        .parameterListHeading("Positional Parameters%n");
        .footerHeading("Footer heading%n")
        .footer("footer line 1", "footer line 2");

The ParserSpec can be used to control the behaviour of the parser to some extent.

2.1.3. Parser Options

cmd.parser()
        .unmatchedArgumentsAllowed(true)
        .overwrittenOptionsAllowed(true);

2.1.4. Mixins

CommandSpec has methods to add options (OptionSpec objects) and positional parameters (PositionalParamSpec objects). A CommandSpec can be mixed in with another CommandSpec, so its options, positional parameters and usage help attributes are merged into the other CommandSpec.

CommandSpec standardHelpOptions = CommandSpec.create()
    .addOption(OptionSpec.builder("-h", "--help")
        .usageHelp(true)
        .description("Show this help message and exit.").build())
    .addOption(OptionSpec.builder("-V", "--version")
        .versionHelp(true)
        .description("Print version information and exit.").build());

CommandSpec cmd = CommandSpec.create()
    .name("mycommand")
    .addMixin("standardHelpOptions", standardHelpOptions);

Actually, since these options are extremely common, CommandSpec provides a convenience method to quickly add these standard help options:

CommandSpec spec = CommandSpec.create();
spec.mixinStandardHelpOptions(true); // usageHelp and versionHelp options

2.1.5. Subcommands

Finally, CommandSpec objects can be subcommands of other CommandSpecs. There is no limit to the depth of a hierarchy of command and subcommands. CommandSpec also allows registration of type converters that are used while parsing the command line arguments to convert a command line argument string to the strongly typed value of a OptionSpec or PositionalParamSpec

CommandSpec helpSubcommand = CommandSpec.forAnnotatedObject(
        new picocli.CommandLine.HelpCommand());

CommandSpec cmd = CommandSpec.create()
    .name("mycommand")
    .addSubcommand("help", helpSubcommand);

2.2. OptionSpec

OptionSpec models a command option. An OptionSpec must have at least one name, which is used during parsing to match command line arguments. Other attributes can be left empty and picocli will give them a reasonable default value. This defaulting is why OptionSpec objects are created with a builder: this allows you to specify only some attributes and let picocli initialise the other attributes. For example, if only the option’s name is specified, picocli assumes the option takes no parameters (arity = 0), and is of type boolean. Another example, if arity is larger than 1, picocli sets the type to List and the auxiliary type to String.

Once an OptionSpec is constructed, its configuration becomes immutable, but its value can still be modified. Usually the value is set during command line parsing when a command line argument matches one of the option names.

The value is set via a binding. We’ll come back to bindings later in this document.

Similar to the annotation API, OptionSpec objects have help, usageHelp and versionHelp attributes. When the parser matches an option that was marked with any of these attributes, it will no longer validate that all required arguments exist. See the section below on the parseWithHandler(s) and parseWithSimpleHandler(s) methods that automatically print help when requested.

2.3. PositionalParamSpec

PositionalParamSpec objects don’t have names, but have an index range instead. A single PositionalParamSpec object can capture multiple positional parameters. The default index range is set to 0..* (all indices). A command may have multiple PositionalParamSpec objects to capture positional parameters at different index ranges. This can be useful if positional parameters at different index ranges have different data types.

Similar to OptionSpec objects, Once a PositionalParamSpec is constructed, its configuration becomes immutable, but its value can still be modified. Usually the value is set during command line parsing when a non-option command line argument is encountered at a position in its index range.

The value is set via a binding. We’ll look at bindings next.

2.4. Bindings

Bindings decouple the option and positional parameter specification from the place where their value is held.

Option specifications and positional parameter specifications created from annotated objects have a FieldBinding (and in the near future they can have a MethodBinding), so when the value is set on an option specification, the field’s value is set (or the setter method is invoked).

Option specifications and positional parameter specifications created programmatically without annotated object by default have an ObjectBinding that simply stores the value in a field of the ObjectBinding.

You may create a custom binding that delegates to some other data structure to retrieve and store the value.

Below is the IBinding interface definition:

public static interface IBinding {

    /** Returns the current value of the binding. For multi-value options and positional
     * parameters, this method returns an array, collection or map to add values to.
     * @throws PicocliException if a problem occurred while obtaining the current value
     */
    <T> T get() throws PicocliException;

    /** Sets the new value of the binding. For multi-value options and positional
     * parameters, this method is used to set a new array instance that is one element
     * larger than the previous instance, or to initialize the collection or map when
     * the {@link #get() getter} returned {@code null}. For single-value options and
     * positional parameters, this method simply sets the value.
     *
     * @param value the new value of the binding
     * @param <T> type of the value
     * @return the previous value of the binding (if supported by this binding)
     * @throws PicocliException if a problem occurred while setting the new value
     */
    <T> T set(T value) throws PicocliException;
}

3. Parse Result

For the below examples, we use the following parser configuration:

CommandSpec spec = CommandSpec.create();
spec.addOption(OptionSpec.builder("-V", "--verbose").build());
spec.addOption(OptionSpec.builder("-f", "--file")
        .paramLabel("FILES")
        .type(List.class)
        .auxiliaryTypes(File.class) // so, this option is of type List<File>
        .description("The files to process").build());
CommandLine commandLine = new CommandLine(spec);

3.1. Querying for Options

The CommandLine::parseArgs method returns a ParseResult object that allows client code to query which options and positional parameters were matched for a given command.

String[] args = { "--verbose", "-f", "file1", "--file=file2" };
ParseResult pr = commandLine.parseArgs(args);

List<String> originalArgs = pr.originalArgs(); // lists all command line args
assert Arrays.asList(args).equals(originalArgs);

assert pr.hasOption("--verbose"); // as specified on command line
assert pr.hasOption("-V");        // other aliases work also
assert pr.hasOption('V');         // single-character alias works too
assert pr.hasOption("verbose");   // and, command name without hyphens

3.2. Typed Option Values

The optionValue method returns the command line value or values, converted to the option’s type. This method requires a default value, which will be returned in case the option was not specified on the command line. In the above example, we defined the --file option to be of type List<File>, so we pass in an empty list as the default value:

ParseResult pr = commandLine.parseArgs("-f", "file1", "--file=file2");

List<File> defaultValue = Collections.emptyList();
List<File> expected     = Arrays.asList(new File("file1"), new File("file2"));

assert expected.equals(pr.optionValue('f', defaultValue));
assert expected.equals(pr.optionValue("--file", defaultValue));

3.3. Untyped Option Values

The rawOptionValue method returns the String argument specified on the command line. If the command line contains multiple arguments for this option, the first value is returned. Use the rawOptionValues method to get a list of all values specified on the command line for an option.

assert "file1".equals(pr.rawOptionValue('f'));       // single-character alias
assert "file1".equals(pr.rawOptionValue("-f"));      // short name
assert "file1".equals(pr.rawOptionValue("--file"));  // long name

List<String> expected = Arrays.asList("file1", "file2");
assert expected.equals(pr.rawOptionValues("f"));     // short name without hyphens
assert expected.equals(pr.rawOptionValues("file"));  // long name without hyphens

3.4. Subcommands

Use the hasSubcommand method to determine whether the command line contained subcommands. The subcommand method returns a different ParseResult object that can be used to query which options and positional parameters were matched for the subcommand.

class App {
    @Option(names = "-x") String x;
}
class Sub {
    @Parameters String[] all;
}
CommandLine cmd = new CommandLine(new App());
cmd.addSubcommand("sub", new Sub());
ParseResult parseResult = cmd.parseArgs("-x", "xval", "sub", "1", "2", "3");

assert parseResult.hasOption("-x");
assert "xval".equals(parseResult.optionValue("-x", "default"));

assert parseResult.hasSubcommand();
ParseResult subResult = parseResult.subcommand();

assert  subResult.hasPositional(0);
assert  subResult.hasPositional(1);
assert  subResult.hasPositional(2);
assert !subResult.hasPositional(3);
assert "1".equals(subResult.rawPositionalValue(0));
assert "2".equals(subResult.rawPositionalValue(1));
assert "3".equals(subResult.rawPositionalValue(2));

4. Parsing and Result Processing

4.1. Basic Processing

The most basic way to parse the command line is to call the CommandLine::parseArgs method and inspect the resulting ParseResult object.

For example:

CommandSpec spec = CommandSpec.create();
// add options and positional parameters

CommandLine commandLine = new CommandLine(spec);
try {
    ParseResult pr = commandLine.parseArgs(args);
    if (CommandLine.printHelpIfRequested(pr)) {
        return;
    }
    int count = pr.optionValue('c', 1);
    List<File> files = pr.positionalValue(0, Collections.<File>emptyList());
    for (int i = 0; i < count; i++) {
        for (File f : files) {
            System.out.printf("%d: %s%n", i, f);
        }
    }
} catch (ParseException invalidInput) {
    System.err.println(invalidInput.getMessage());
    invalidInput.getCommandLine().usage(System.err);
}

4.2. Convenience Methods

There are a number of parseWithHandler convenience methods to reduce some boilerplate when processing the ParseResult programmatically. The convenience methods take care of printing help when requested by the user, and handle invalid input.

4.2.1. Handlers Without Return Value

Call the parseWithSimpleHandler method with a AbstractSimpleParseResultHandler subclass to process the parse result without returning a result value. Note the absence of error handling and checking of whether the user requested help. The handle method contains only your business logic.

Example:

CommandSpec spec = CommandSpec.create();
// add options and positional parameters

CommandLine commandLine = new CommandLine(spec);
commandLine.parseWithSimpleHandler(new AbstractSimpleParseResultHandler() {
    public void handle(ParseResult pr) {
        int count = pr.optionValue('c', 1);
        List<File> files = pr.positionalValue(0, Collections.<File>emptyList());
        for (int i = 0; i < count; i++) {
            for (File f : files) {
                System.out.printf("%d: %s%n", i, f);
            }
        }
    }
}, args);

A variation of this method, parseWithSimpleHandlers, additionally takes an IExceptionHandler2<Void> to customize how invalid input should be handled and optionally set an exit code for when the input was invalid.

Example:

CommandSpec spec = CommandSpec.create();
// add options and positional parameters

CommandLine commandLine = new CommandLine(spec);
commandLine.parseWithSimpleHandlers(new AbstractSimpleParseResultHandler() {
    public void handle(ParseResult pr) {...}
}.useOut(System.out).andExit(123),
        new DefaultExceptionHandler<Void>().andExit(567),
        args);

4.2.2. Handlers with Return Value

It is possible for the parse result processing logic to return a result. To accomplish this, call the CommandLine::parseWithHandler method with a class that extends AbstractParseResultHandler and a prototype return value. The process method may return a completely different return value object (as long as it has the correct type) or it can modify the return value object that was passed in to the parseWithHandler method.

Example:

CommandSpec spec = CommandSpec.create();
// add options and positional parameters

CommandLine commandLine = new CommandLine(spec);

class MyResult {
    List<File> files = new ArrayList<File>();
}

class MyHandler extends AbstractParseResultHandler<MyResult> {
    public MyResult handle(ParseResult pr, MyResult returnValue) {
        int count = pr.optionValue('c', 1);
        List<File> files = pr.positionalValue(0, Collections.<File>emptyList());
        for (File f : files) {
            for (int i = 0; i < count; i++) {
                returnValue.files.add(f);
            }
        }
        return returnValue;
    }
    protected MyHandler self() { return this; }
}

MyResult result = commandLine.parseWithHandler(new MyHandler(), new MyResult(), args);
// do something with result...

This method also has a variation, parseWithHandlers, which additionally takes an IExceptionHandler2<MyResult> to customize how invalid input should be handled and optionally set an exit code.

Example:

CommandSpec spec = CommandSpec.create();
// add options and positional parameters

CommandLine commandLine = new CommandLine(spec);
MyResult result = commandLine.parseWithHandler(
        new MyHandler().useOut(System.out).andExit(123),
        new MyResult(),
        new DefaultExceptionHandler<MyResult>().andExit(567),
        args);
// do something with result...