Picocli 2.0: Do More With Less
Introduction
Picocli is a one-file command line parsing framework that allows you to create command line applications with almost no code. Annotate fields in your application with @Option
or @Parameters
, and picocli will populate these fields with command line options and positional parameters respectively. For example:
@Command(name = "Greet", header = "%n@|green Hello world demo|@") class Greet implements Runnable { @Option(names = {"-u", "--user"}, required = true, description = "The user name.") String userName; public void run() { System.out.println("Hello, " + userName); } public static void main(String... args) { CommandLine.run(new Greet(), System.err, args); } }
When we execute this program, picocli parses the command line and populates the userName
field before invoking the run
method:
$ java Greet -u picocli Hello, picocli
Picocli generates usage help messages with Ansi colors and styles. If we run the above program with invalid input (missing the required user name option), picocli prints an error and the usage help message:
Picocli can generate an autocompletion script that allows end users to use <TAB>
command line completion to discover which options and subcommands are available. You may also like picocli’s support for subcommands and nested sub-subcommands to any level of depth.
The user manual describes picocli’s functionality in detail. This article highlights new and noteworthy features introduced with the picocli 2.0 release.
Mixing Options With Positional Parameters
The parser has been improved and positional parameters can now be mixed with options on the command line.
Previously, positional parameters had to follow options. From this release, any command line argument that is not an option or subcommand is interpreted as a positional parameter.
For example:
class MixDemo implements Runnable { @Option(names = "-o") List<String> options; @Parameters List<String> positional; public void run() { System.out.println("positional: " + positional); System.out.println("options : " + options); } public static void main(String[] args) { CommandLine.run(new MixDemo(), System.err, args); } }
Running the above class with a mixture of options and positional parameters shows that non-options are recognized as positional parameters. For example:
$ java MixDemo param0 -o AAA param1 param2 -o BBB param3 positional: [param0, param1, param2, param3] options : [AAA, BBB]
To support mixing options with positional parameters, the parser has changed. From picocli 2.0, multi-value options (array, list and map fields) are not greedy by default any more. The 2.0 release notes describe this change and other potential breaking changes in detail.
Discovering Collection Types
Picocli performs automatic type conversion of command line arguments to the type of the annotated field. Both named options and positional parameters can be strongly typed.
Prior to v2.0, picocli needed Collection
and Map
fields to be annotated with the type
attribute to be able to do type conversion. For fields with other types, like array fields and single-value fields like int
or java.io.File
fields, picocli automatically detects the target type from the field type, but collections and maps needed more verbose annotation. For example:
class Before { @Option(names = "-u", type = {TimeUnit.class, Long.class}) Map<TimeUnit, Long> timeout; @Parameters(type = File.class) List<File> files; }
From v2.0, the type
attribute is no longer necessary for Collection
and Map
fields: picocli will infer the collection element type from the generic type. The type
attribute still works as before, it is just optional in most cases.
Omitting the type
attribute removes some duplication and results in simpler and cleaner code:
class Current { @Option(names = "-u") Map<TimeUnit, Long> timeout; @Parameters List<File> files; }
In the above example, picocli 2.0 is able to automatically discover that command line arguments need to be converted to File
before adding them to the list, and for the map, that keys need to be converted to TimeUnit
and values to Long
.
Automatic Help
Picocli provides a number of convenience methods like run
and call
that parse the command line arguments, take care of error handling, and invoke an interface method to execute the application.
From this release, the convenience methods will also automatically print usage help and version information when the user specifies an option annotated with the versionHelp
or usageHelp
attribute on the command line.
The example program below demonstrates automatic help:
@Command(version = "Help demo v1.2.3", header = "%nAutomatic Help Demo%n", description = "Prints usage help and version help when requested.%n") class AutomaticHelpDemo implements Runnable { @Option(names = "--count", description = "The number of times to repeat.") int count; @Option(names = {"-h", "--help"}, usageHelp = true, description = "Print usage help and exit.") boolean usageHelpRequested; @Option(names = {"-V", "--version"}, versionHelp = true, description = "Print version information and exit.") boolean versionHelpRequested; public void run() { // NOTE: code like below is no longer required: // // if (usageHelpRequested) { // new CommandLine(this).usage(System.err); // } else if (versionHelpRequested) { // new CommandLine(this).printVersionHelp(System.err); // } else { ... the business logic for (int i = 0; i < count; i++) { System.out.println("Hello world"); } } public static void main(String... args) { CommandLine.run(new AutomaticHelpDemo(), System.err, args); } }
When executed with -h
or --help
, the program prints usage help:
Similarly, when executed with -V
or --version
, the program prints version information:
Methods that automatically print help:
- CommandLine::call
- CommandLine::run
- CommandLine::parseWithHandler (with the built-in Run… handlers)
- CommandLine::parseWithHandlers (with the built-in Run… handlers)
Methods that do not automatically print help:
- CommandLine::parse
- CommandLine::populateCommand
Better Subcommand Support
This release adds new CommandLine::parseWithHandler
methods. These methods offer the same ease of use as the run
and call
methods, but with more flexibility and better support for nested subcommands.
Consider what an application with subcommands needs to do:
- Parse the command line.
- If user input was invalid, print the error message and the usage help message for the subcommand where the parsing failed.
- If parsing succeeded, check if the user requested usage help or version information for the top-level command or a subcommand. If so, print the requested information and exit.
- Otherwise, execute the business logic. Usually this means executing the most specific subcommand.
Picocli provides some building blocks to accomplish this, but it was up to the application to wire them together. This wiring is essentially boilerplate and is very similar between applications. For example, previously, an application with subcommands would typically contain code like this:
public static void main() { // 1. parse the command line CommandLine top = new CommandLine(new YourApp()); List<CommandLine> parsedCommands; try { parsedCommands = top.parse(args); } catch (ParameterException ex) { // 2. handle incorrect user input for one of the subcommands System.err.println(ex.getMessage()); ex.getCommandLine().usage(System.err); return; } // 3. check if the user requested help for (CommandLine parsed : parsedCommands) { if (parsed.isUsageHelpRequested()) { parsed.usage(System.err); return; } else if (parsed.isVersionHelpRequested()) { parsed.printVersionHelp(System.err); return; } } // 4. execute the most specific subcommand Object last = parsedCommands.get(parsedCommands.size() - 1).getCommand(); if (last instanceof Runnable) { ((Runnable) last).run(); } else if (last instanceof Callable) { Object result = ((Callable) last).call(); // ... do something with result } else { throw new ExecutionException("Not a Runnable or Callable"); } }
This is quite a lot of boilerplate code. Picocli 2.0 provides a convenience method that allows you to reduce all of the above to a single line of code so you can focus on the business logic of your application:
public static void main() { // This handles all of the above in one line: // 1. parse the command line // 2. handle incorrect user input for one of the subcommands // 3. automatically print help if requested // 4. execute one or more subcommands new CommandLine(new YourApp()).parseWithHandler(new RunLast(), System.err, args); }
The new convenience method is parseWithHandler
. You can create your own custom handler or use one of the built-in handlers. Picocli provides handler implementations for some common use cases.
The built-in handlers are RunFirst
, RunLast
and RunAll
. All of these provide automatic help: if the user requests usageHelp or versionHelp, the requested information is printed and the handler returns without further processing. The handlers expect all commands to implement either java.lang.Runnable
or java.util.concurrent.Callable
.
RunLast
executes the most specific command or subcommand. For example, if the user invokedjava Git commit -m "commit message"
, picocli considersGit
the top-level command andcommit
a subcommand. In this example, thecommit
subcommand is the most specific command, soRunLast
would only execute that subcommand. If there are no subcommands, the top-level command is executed.RunLast
is now used internally by picocli to implement the existingCommandLine::run
andCommandLine::call
convenience methods.RunFirst
only executes the first, top-level, command and ignores subcommands.RunAll
executes the top-level command and all subcommands that appeared on the command line.
There is also a parseWithHandlers
method, which is similar but additionally lets you specify a custom handler for incorrect user input.
Improved run
and call
Methods
The CommandLine::call
and CommandLine::run
convenience methods now support subcommands and will execute the last subcommand specified by the user. Previously subcommands were ignored and only the top-level command was executed.
Improved Exceptions
Finally, from this release, all picocli exceptions provide a getCommandLine
method that returns the command or subcommand where parsing or execution failed. Previously, if the user provided invalid input for applications with subcommands, it was difficult to pinpoint exactly which subcommand failed to parse the input.
Conclusion
If you are already using picocli, v2.0 is an essential upgrade. If you haven’t used picocli before, I hope the above made you interested to give it a try.
Many of these improvements originated in user feedback and subsequent discussions. Please don’t hesitate to ask questions, request features or give other feedback on the picocli issue tracker.
Please star the project on GitHub if you like it and tell your friends!
Published on Java Code Geeks with permission by Remko Popma, partner at our JCG program. See the original article here: Picocli 2.0: Do More With Less Opinions expressed by Java Code Geeks contributors are their own. |