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.