Picocli subcommands
One program, many purposes
This tutorial focuses on the use of subcommands in picocli programs. Using subcommands allows you to expose more functionality through a single command-line application in a user-friendly way. If you are not yet familiar with picocli, you may want to visit the picocli introduction.
The Unix philosophy includes “Do One Thing and Do It Well. This is generally sound advice and is followed by most of the base Unix programs. Some more advanced programs do deviate from this and includes a variety of functionality inside a single executable.
Examples of such programs are git
and docker
. The git
command includes all the functionality to interact with Git repositories and docker
has a multitude of functionality related to a Docker environment (managing images, containers and resources such as volumes and networks).
The approach here is to use subcommands. For example git clone
will create a local repository as a clone of a remote repository. There are multiple subcommands of git
, such as git commit
, git branch
and many more. Each subcommand also come with its own set of options and parameters. This is a case of diverse functionality around common data (Git repositories).
Subcommands in picocli
From the user’s perspective, subcommands in picocli applications are similar to working with git
and docker
. The subcommand is specifed as a parameter to the main application. Arguments passed before the subcommand name are considered to belong to the main command. Arguments passed after the subcommand, belong to the subcommand.
For the application developers, there are two primary implementation options. The first option is to use methods annotated with @Command
in a single class. The other option is to use distinct classes annotated with @Command
for each subcommand. Which approach to use will depend on your application and the style of design.
For the following examples, we will provide a program with the three subcommands prepare
, process
and status
. This is a fairly simple view of the most basic functionality in programs using subcommands. For simplicity, the application will simply write a message to the console. In the real world, the implementations are highly application-dependent, typically involving some kind of file processing and/or connections to one or more external systems (database servers, online resources, messaging systems, etc).
Subcommands with methods
Perhaps the simplest way to add subcommands is by using methods on the existing command class. The example below contains a main method along with the main @Command
annotation on the class. The class has three methods annotated with @Command
as well. These are the three subcommands prepare
, process
and status
.
import java.util.concurrent.Callable;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(name = "subcommands")
public class SubcommandsMethods implements Callable<Integer> {
@Command(name = "prepare")
public Integer prepare() {
System.out.println("Preparing data files.");
return 0;
}
@Command(name = "process")
public Integer process() {
System.out.println("Processing data files.");
return 0;
}
@Command(name = "status")
public void status() {
System.out.println("Show status of data files.");
}
@Override
public Integer call() {
System.out.println("Subcommand needed: 'prepare', 'process' or 'status'");
return 0;
}
public static void main(String[] args) {
int rc = new CommandLine(new SubcommandsMethods()).execute(args);
System.exit(rc);
}
}
When we invoke the main method of this class, it will create a new instance of CommandLine
, passing in an instance of our class. Based on the instance of our class, picocli can determine the options and parameters supported by our application. Next it executes our main command, passing in the command-line arguments. As our main command class implements Callable<Integer>
, picocli will default to invoke the Integer call()
method on our class.
Since we also have three subcommands, picocli will check the command-line arguments for parameters matching one of the subcommand names. If a match is found, then the corresponding method is executed instead of the default call()
method.
In the main()
method, we exit with the return code returned from picocli.
The return code of our main command is coming from the return value of the call()
method, which return an Integer
. For the subcommands, the prepare
and process
also both return an Integer
. In these cases, the value returned from the subcommand method is used as the return code. For the status
method, the return type is void
. In this case, the default return code 0
is returned. If the subcommand method throws an exception, then the default return code is 1
.
Subcommand methods with options and parameters
We covered return codes for subcommands with methods but how do we handle options and parameters? Similar to how we’ve moved the @Command
annotation from the class level to the method level, we can use the @Option
and @Parameters
annotations with method parameters. This way, we can specify the options and parameters for the subcommand by using annotated parameters.
The example below adds the option -n
to the process
subcommand and also reads in a single parameter labelled INPUTDIR
.
@Command(name = "process")
public Integer process(
@Option(names = {"-n"}, defaultValue = "-1") int maxEntries,
@Parameters(arity = "1", paramLabel = "INPUTDIR") Path inputDir) {
System.out.println("Processing data files.");
return 0;
}
Referencing arguments to the main command
There may be situations, were a subcommand wish to query options passed to the main command. An example of an option passed to the main command but impacting subcommand behaviour is the --git-dir
option which can be used with git
. The default behaviour of git
is to look for the Git repository in the current working directory. The --git-dir
option overrides this, so that the specified directory is used as the Git repository. Since all git
subcommands require a Git repository to work with, the option is placed as an argument to the main command, rather than added to each of the subcommands individually.
Another typical example is to have a global option, which enables verbose output. When enabled, the application will report more details on how and what it is processing. This is typically enabled with a flag option named -v
(short version) or --verbose
(long version).
The option is added as a new field to the SubcommandsMethods
class. Since it is a flag, we will specify the type as boolean
. The default value is set to false.
@Option(names = { "-v", "--verbose" }, defaultValue = "false")
private boolean verbose = false;
Since the subcommands are implemented as methods in the same class as the main command, the field can be accessed directly. Below is the extended process
method, which outputs more information, if the verbose flag is true.
@Command(name = "process")
public Integer process(@Option(names = { "-n" }, defaultValue = "-1") int maxEntries,
@Parameters(arity = "1", paramLabel = "INPUTDIR") Path inputDir) {
System.out.println("Processing data files.");
if (verbose) {
System.out.printf("Processing files in %s%n", inputDir);
if (maxEntries > 0) {
System.out.printf("Max entries: %d%n", maxEntries);
}
}
return 0;
}
Subcommands as separate classes
The other alternative for implementing subcommands is to use separate classes. The primary reasons for using classes over methods is to increase cohesion (this is the classic software design principle of high cohesion / low coupling). As more and more arguments are added to each subcommand and the processing becomes increasingly complicated (new business rules, corner cases, exceptional error handling, etc), the approach with methods tends to lead to increasingly large classes, with an adverse impact on maintainability.
The subcommands prepare
, process
and status
will now be implemented as separate top-level classes. It is also possible to use static nested classes but with the objectives of high cohesion and maintainability, it is most often recommendable to use top-level classes.
We will start by creating a new class for the prepare
subcommand.
import java.util.concurrent.Callable;
import picocli.CommandLine.Command;
@Command(name = "prepare")
public class SubcommandPrepare implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("Preparing data files.");
return 0;
}
}
Notice that the new class SubcommandPrepare
implements Callable<Integer>
and the functionality previously found in the public Integer prepare()
method is now in the public Integer call()
method. The @Command
annotation has also been moved from method-level to the class-level.
Next, we implement the process
subcommand. We will start with the simpler version, which does not reference the option from the main command.
import java.nio.file.Path;
import java.util.concurrent.Callable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "process")
public class SubcommandProcess implements Callable<Integer> {
@Option(names = { "-n" }, defaultValue = "-1")
private int maxEntries;
@Parameters(arity = "1", paramLabel = "INPUTDIR")
private Path inputDir;
@Override
public Integer call() {
System.out.println("Processing data files.");
return 0;
}
}
The process
subcommand accepts the option -n
and requires a parameter for the input directory. With subcommands as classes, the @Option
and @Parameters
annotations work exactly as they do with main commands.
The final subcommand is status
, which differs from the two previous subcommands by not having a return value.
import java.util.concurrent.Callable;
public class SubcommandStatus implements Callable<Void>{
@Override
public Void call() {
System.out.println("Show status of data files.");
return null;
}
}
Here we are still implementing Callable
but the type parameter has been changed from Integer
to Void
(java.lang.Void
). The Void
type is a bit special as we can’t create instances. As a consequence and since we don’t intend to return anything, we return null
.
Since we are returning void
, we could also choose to implement Runnable
instead of Callable<Void>
.
public class SubcommandStatus implements Runnable {
@Override
public void run() {
System.out.println("Show status of data files.");
}
}
Now that we have separate classes for all the subcommands, it is time to provide the main command. The main command is also in a separate class and is almost indistinguishable from a command without subcommands.
import java.util.concurrent.Callable;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(name = "subcommands", subcommands = { SubcommandPrepare.class,
SubcommandProcess.class, SubcommandStatus.class })
public class SubcommandsClasses implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("Subcommand needed: 'prepare', 'process' or 'status'");
return 0;
}
public static void main(String[] args) {
int rc = new CommandLine(new SubcommandsMethods()).execute(args);
System.exit(rc);
}
}
The main()
and call()
methods are similar to those found in the version with subcommands in methods. What has changed is that we no longer have the subcommand methods but have added the subcommands
parameter to the @Command
annotation. The value for this parameter is an array of the classes with subcommands.
The SubcommandsMethods
and SubcommandsClasses
are now nearly functionally equivalent from the users perspective. The only thing missing is the option for verbose output in the main command.
Reference to the main command
The subcommands are now in classes separate from the main command. So how do we access the arguments passed to the main command? In the version with methods, it was simply accessing the field in the class. With the main and subcommands in separate classes, we no longer have direct access to the main command. This is addressed in picocli through the @ParentCommand
annotation. Using this on a field in the subcommand class, we can get access to the main (parent) command.
First add the --verbose
option to the SubcommandsClasses
class.
@Option(names = { "-v", "--verbose" }, defaultValue = "false")
private boolean verbose = false;
public boolean isVerbose() {
return verbose;
}
The field verbose
along with the @Option
annotation is similar to the one we added to the SubcommandsMethods
class. The method isVerbose()
is added, so that the field can be accessed outside the class.
In order to access the option in the subcommand class, we need to get a reference to the main command instance. The updated SubcommandProcess
is below.
import java.nio.file.Path;
import java.util.concurrent.Callable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParentCommand;
@Command(name = "process")
public class SubcommandProcess implements Callable<Integer> {
@Option(names = { "-n" }, defaultValue = "-1")
private int maxEntries;
@Parameters(arity = "1", paramLabel = "INPUTDIR")
private Path inputDir;
@ParentCommand
private SubcommandsClasses mainCmd;
@Override
public Integer call() {
System.out.println("Processing data files.");
if (mainCmd.isVerbose()) {
System.out.printf("Processing files in %s%n", inputDir);
if (maxEntries > 0) {
System.out.printf("Max entries: %d%n", maxEntries);
}
}
return 0;
}
}
Notice the new field mainCmd
of type SubcommandsClasses
and with the annotation @ParentCommand
. With the annotation present, picocli will initialize the field with a reference to the SubcommandsClasses
instance. Since there is now a method to read the option value (isVerbose()
), we can use the mainCmd
field to query the value of the --verbose
parameter passed to the main command.
Nesting subcommands
It is also possible to nest subcommands within other subcommands. Nested subcommands are well-known with docker
where often the first subcommand is the resource type (image, network, volume) and the second subcommand is the operation on the resource. Examples include docker volume create
(create a new docker volume) and docker volume ls
(list existing docker volumes).
In picocli it is possible to create nested commands with the @Command
annotation. We could create subcommands to the SubcommandProcess
class by using the subcommands
annotation parameter (similar to the SubcommandsClasses
class) or by annotating methods in the class with @Command
.
More possibilities with subcommands
The support for subcommands in picocli goes beyond the areas covered above. Some of the other features, which may be of interest, includes support for hidden subcommands, inherited options and abbreviated options and subcommands. The official documentation has more information on these topics.