Micronaut Picocli Configuration

Provides integration between Micronaut and Picocli

Version: 5.3.0

1 Introduction

Picocli is a command line parser that supports usage help with ANSI colors, autocomplete and nested subcommands. It has an annotations API to create command line applications with almost no code, and a programmatic API for dynamic uses like creating Domain Specific Languages.

From the project Readme page:

How it works: annotate your class and picocli initializes it from the command line arguments, converting the input to strongly typed data. Supports git-like subcommands (and nested sub-subcommands), any option prefix style, POSIX-style grouped short options, password options, custom type converters and more. Parser tracing facilitates troubleshooting.

It distinguishes between named options and positional parameters and allows both to be strongly typed. Multi-valued fields can specify an exact number of parameters or a range (e.g., 0..*, 1..2). Supports Map options like -Dkey1=val1 -Dkey2=val2, where both key and value can be strongly typed.

It generates polished and easily tailored usage help and version help, using ANSI colors where possible. Picocli-based command line applications can have TAB autocompletion, interactively showing users what options and subcommands are available. Picocli can generate completion scripts for bash and zsh, and offers an API to easily create a JLine Completer for your application.

Micronaut features dedicated support for defining picocli Command instances. Micronaut applications built with picocli can be deployed with or without the presence of an HTTP server.

Combining picocli with Micronaut makes it easy to provide a rich, well-documented command line interface for your Microservices.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Create Micronaut Picocli app

You can create a Micronaut command line interface application using the Micronaut CLI:

Example 1. Using the CLI
$ mn create-cli-app my-app
launch

4 Setting up Picocli

To add support for Picocli to an existing project, you should first add the picocli dependency and the Micronaut picocli configuration to your build configuration.

implementation("io.micronaut.picocli:micronaut-picocli")
<dependency>
    <groupId>io.micronaut.picocli</groupId>
    <artifactId>micronaut-picocli</artifactId>
</dependency>

The picocli-codegen module includes an annotation processor that can build a model from the picocli annotations at compile time rather than at runtime. Enabling this annotation processor in your project is optional, but recommended.

annotationProcessor("info.picocli:picocli-codegen")
<annotationProcessorPaths>
    <path>
        <groupId>info.picocli</groupId>
        <artifactId>picocli-codegen</artifactId>
    </path>
</annotationProcessorPaths>

The picocli-codegen annotation processor is incompatible with the Kotlin KSP compiler plugin. Using it in a Kotlin project requires the Kotlin Kapt compiler plugin instead.

Configuring picocli

Picocli does not require configuration. See other sections of the manual for configuring the services and resources to inject.

5 Generating a Picocli Project

To create a project with picocli support using the Micronaut CLI, use the create-cli-app command. This will add the dependencies for the picocli feature, and set the applicationType of the generated project to cli, so the create-command command is available to generate additional commands.

The main class of the project is set to the *Command class (based on the project name - e.g., hello-world will generate HelloWorldCommand):

$ mn create-cli-app my-cli-app

The generated command looks like this:

my.cli.app.MyCliAppCommand.java generated by create-cli-app
import io.micronaut.configuration.picocli.PicocliRunner;
import org.slf4j.Logger;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

import static org.slf4j.LoggerFactory.getLogger;

@Command(name = "my-cli-app", description = "...", mixinStandardHelpOptions = true) // (1)
public class MyCliAppCommand implements Runnable { // (2)
    private static final Logger LOG = getLogger(MyCliAppCommand.class);

    @Option(names = {"-v", "--verbose"}, description = "...") // (3)
    boolean verbose;

    public static void main(String[] args) throws Exception {
        PicocliRunner.run(MyCliAppCommand.class, args); // (4)
    }

    public void run() { // (5)
        // business logic here
        if (verbose) {
            LOG.info("Hi!");
        }
    }
}
my.cli.app.MyCliAppCommand.java generated by create-cli-app
import io.micronaut.configuration.picocli.PicocliRunner
import org.slf4j.Logger
import picocli.CommandLine.Command
import picocli.CommandLine.Option

import static org.slf4j.LoggerFactory.getLogger

@Command(name = 'my-cli-app', description = '...', mixinStandardHelpOptions = true) // (1)
class MyCliAppCommand implements Runnable { // (2)
    private static final Logger LOG = getLogger(MyCliAppCommand.class)

    @Option(names = ['-v', '--verbose'], description = '...') // (3)
    boolean verbose

    static void main(String[] args) throws Exception {
        PicocliRunner.run(MyCliAppCommand.class, args) // (4)
    }

    void run() { // (5)
        // business logic here
        if (verbose) {
            LOG.info("Hi!")
        }
    }
}
my.cli.app.MyCliAppCommand.java generated by create-cli-app
import io.micronaut.configuration.picocli.PicocliRunner
import org.slf4j.LoggerFactory
import picocli.CommandLine.Command
import picocli.CommandLine.Option

@Command(name = "my-cli-app", description = ["..."], mixinStandardHelpOptions = true) // (1)
class MyCliAppCommand : Runnable { // (2)

    @Option(names = ["-v", "--verbose"], description = ["..."]) // (3)
    var verbose = false

    companion object {
        private val LOG = LoggerFactory.getLogger(MyCliAppCommand::class.java)

        @Throws(Exception::class)
        @JvmStatic
        fun main(args: Array<String>) {
            PicocliRunner.run(MyCliAppCommand::class.java, *args) // (4)
        }
    }

    override fun run() { // (5)
        // business logic here
        if (verbose) {
            LOG.info("Hi!")
        }
    }
}
1 The picocli @Command annotation designates this class as a command. The mixinStandardHelpOptions attribute adds --help and --version options to it.
2 By implementing Runnable or Callable your application can be executed in a single line (<4>) and picocli takes care of handling invalid input and requests for usage help (<cmd> --help) or version information (<cmd> --version).
3 An example option. Options can have any name and be of any type. The generated code contains this example boolean flag option that lets the user request more verbose output.
4 PicocliRunner lets picocli-based applications leverage the Micronaut DI container. PicocliRunner.run first creates an instance of this command with all services and resources injected, then parses the command line, while taking care of handling invalid input and requests for usage help or version information, and finally invokes the run method.
5 Put the business logic of the application in the run or call method.

Running the Application

Now you can build the project and start the application. Generate an executable Jar. When you run this JAR, it executes the MyCliAppCommand command.

With Gradle:

$ ./gradlew shadowJar
$ java -jar build/libs/my-cli-app-0.1-all.jar -v

With Maven Package:

$ ./mvnw package
$ java -jar target/my-cli-app-0.1.jar -v

6 Picocli Quick Start

Creating a Picocli Command with @Command

This section will show a quick example that provides a command line interface to a HTTP client that communicates with the GitHub API.

When creating this example project with the Micronaut CLI, use the create-cli-app command, and add the --features=http-client flag:

$ mn create-cli-app example.git-star --features http-client

This will add the io.micronaut:micronaut-http-client dependency to the build. You can also manually add to your build:

implementation("io.micronaut:micronaut-http-client")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-client</artifactId>
</dependency>

An Example HTTP Client

To create a picocli Command you create a class with fields annotated with @Option or @Parameters to capture the values of the command line options or positional parameters, respectively.

For example the following is a picocli @Command that wraps around the GitHub API:

Example picocli command with injected HTTP client
import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import jakarta.inject.Inject;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Command(name = "git-star", header = {
    "@|green       _ _      _             |@", // (1)
    "@|green  __ _(_) |_ __| |_ __ _ _ _  |@",
    "@|green / _` | |  _(_-<  _/ _` | '_| |@",
    "@|green \\__, |_|\\__/__/\\__\\__,_|_|@",
    "@|green |___/                        |@"},
    description = "Shows GitHub stars for a project",
    mixinStandardHelpOptions = true,
    version = "git-star 0.1") // (2)
public class GitStarCommand implements Runnable {

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

    @Option(names = { "-v", "--verbose" }, description = "Shows some project details")
    boolean verbose;

    @Parameters(  // (4)
        description = {
            "One or more GitHub slugs (comma separated) to show stargazers for. Default: ${DEFAULT-VALUE}"
        },
        split = ",",
        paramLabel = "<owner/repo>"
    )
    List<String> githubSlugs = Arrays.asList("micronaut-projects/micronaut-core", "remkop/picocli");

    public void run() { // (5)
        BlockingHttpClient blockingClient = client.toBlocking();
        for (String slug : githubSlugs) {
            HttpRequest<Object> httpRequest = HttpRequest.GET("/repos/" + slug)
                .header("User-Agent", "remkop-picocli");
            Map<?,?> m = blockingClient.retrieve(httpRequest, Map.class);
            System.out.printf("%s has %s stars%n", slug, m.get("watchers"));

            if (verbose) {
                String msg = "Description: %s%nLicense: %s%nForks: %s%nOpen issues: %s%n%n";
                System.out.printf(msg, m.get("description"),
                    ((Map<?,?>) m.get("license")).get("name"),
                    m.get("forks"), m.get("open_issues"));
            }
        }
    }

    public static void main(String[] args) {
        int exitCode = PicocliRunner.execute(GitStarCommand.class, args);
        System.exit(exitCode);
    }
}
Example picocli command with injected HTTP client
import io.micronaut.configuration.picocli.PicocliRunner
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.*

import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters

import jakarta.inject.Inject

@Command(name = 'git-star', header = [
    "@|green       _ _      _             |@", // (1)
    "@|green  __ _(_) |_ __| |_ __ _ _ _  |@",
    "@|green / _` | |  _(_-<  _/ _` | '_| |@",
    "@|green \\__, |_|\\__/__/\\__\\__,_|_|@",
    "@|green |___/                        |@"],
        description = 'Shows GitHub stars for a project',
        mixinStandardHelpOptions = true,
        version = 'git-star 0.1') // (2)
class GitStarCommand implements Runnable {

    @Client('https://api.github.com')
    @Inject
    HttpClient client // (3)

    @Option(names = ['-v', '--verbose'], description = 'Shows some project details')
    boolean verbose

    @Parameters(  // (4)
        description =  [
            'One or more GitHub slugs (comma separated) to show stargazers for. Default: ${DEFAULT-VALUE}'
        ],
        split = ',',
        paramLabel = '<owner/repo>'
    )
    List<String> githubSlugs = ['micronaut-projects/micronaut-core', 'remkop/picocli']

    void run() { // (5)
        BlockingHttpClient blockingClient = client.toBlocking()
        githubSlugs.each { slug ->
            HttpRequest<Object> httpRequest = HttpRequest.GET("/repos/$slug")
                    .header('User-Agent', 'remkop-picocli')
            Map<?,?> m = blockingClient.retrieve(httpRequest, Map.class)
            println("$slug has ${m.watchers} stars")

            if (verbose) {
                println """Description: ${m.description}
                          |License: ${m.license?.name}
                          |Forks: ${m.forks}
                          |Open issues: ${m.open_issues}
                          |""".stripMargin()
            }
        }
    }

    static void main(String[] args) {
        int exitCode = PicocliRunner.execute(GitStarCommand, args)
        System.exit(exitCode)
    }
}
Example picocli command with injected HTTP client
import io.micronaut.configuration.picocli.PicocliRunner
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import jakarta.inject.Inject
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import kotlin.system.exitProcess

@Command(
    name = "git-star",
    header = [
        "@|green       _ _      _             |@",  // (1)
        "@|green  __ _(_) |_ __| |_ __ _ _ _  |@",
        "@|green / _` | |  _(_-<  _/ _` | '_| |@",
        "@|green \\__, |_|\\__/__/\\__\\__,_|_|@",
        "@|green |___/                        |@"],
    description = ["Shows GitHub stars for a project"],
    mixinStandardHelpOptions = true,
    version = ["git-star 0.1"] // (2)
)
class GitStarCommand : Runnable {

    @Inject
    @field:Client("https://api.github.com/")
    lateinit var client: HttpClient // (3)

    @Option(names = ["-v", "--verbose"], description = ["Shows some project details"])
    var verbose = false

    @Parameters(  // (4)
        description = ["One or more GitHub slugs (comma separated) to show stargazers for. Default: \${DEFAULT-VALUE}"],
        split = ",",
        paramLabel = "<owner/repo>"
    )
    var githubSlugs: List<String> = mutableListOf("micronaut-projects/micronaut-core", "remkop/picocli")

    override fun run() { // (5)
        val blockingClient = client.toBlocking()
        githubSlugs.forEach { slug ->
            val httpRequest = HttpRequest.GET<Any>("repos/$slug")
                .header("User-Agent", "remkop-picocli")
            val m = blockingClient.retrieve(httpRequest, Map::class.java)
            println("$slug has ${m["watchers"]} stars")

            if (verbose) {
                println("""Description: ${m["description"]}
                          |License: ${(m["license"] as Map<*,*>)["name"]}
                          |Forks: ${m["forks"]}
                          |Open issues: ${m["open_issues"]}
                          |""".trimMargin())
            }
        }
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val exitCode = PicocliRunner.execute(GitStarCommand::class.java, *args)
            exitProcess(exitCode)
        }
    }
}
1 Headers, footers and descriptions can be multi-line. You can embed ANSI styled text anywhere with the @|STYLE1[,STYLE2]…​ text|@ markup notation.
2 Add version information to display when the user requests this with --version. This can also be supplied dynamically, e.g. from the manifest file or a build-generated version properties file.
3 Inject a HTTP client. In this case, hard-coded to the GitHub API endpoint.
4 A positional parameter that lets the user select one or more GitHub projects
5 The business logic: display information for each project the user requested.

The usage help message generated for this command looks like this:

picocli example

7 Subcommands

If your service has a lot of functionality, a common pattern is to have subcommands to control different areas of the service. To allow Micronaut to inject services and resources correctly into the subcommands, make sure to obtain subcommand instances from the ApplicationContext, instead of instantiating them directly.

The easiest way to do this is to declare the subcommands on the top-level command, like this:

A top-level command with subcommands
import picocli.CommandLine.Command;
import io.micronaut.configuration.picocli.PicocliRunner;

import java.util.concurrent.Callable;

@Command(name = "topcmd", subcommands = {SubCmd1.class, SubCmd2.class}) // (1)
public class TopCommand implements Callable<Object> { // (2)

    public static void main(String[] args) {
        PicocliRunner.execute(TopCommand.class, args); // (3)
    }

    @Override
    public Object call() throws Exception {
        return "Hi Top Command!";
    }
}

@Command(name = "subcmd1")
class SubCmd1 implements Callable<Object> { // (2)

    @Override
    public Object call() throws Exception {
        return "Hi Sub Command 1!";
    }
}

@Command(name = "subcmd2")
class SubCmd2 implements Callable<Object> { // (2)

    @Override
    public Object call() throws Exception {
        return "Hi Sub Command 2!";
    }
}
A top-level command with subcommands
import picocli.CommandLine.Command
import io.micronaut.configuration.picocli.PicocliRunner

import java.util.concurrent.Callable

@Command(name = 'topcmd', subcommands = [ SubCmd1, SubCmd2 ]) // (1)
class TopCommand implements Callable<Object> { // (2)

    static void main(String[] args) {
        PicocliRunner.execute(TopCommand.class, args) // (3)
    }

    @Override
    Object call() throws Exception {
        'Hi Top Command!'
    }
}

@Command(name = 'subcmd1')
class SubCmd1 implements Callable<Object> { // (2)

    @Override
    Object call() throws Exception {
        'Hi Sub Command 1!'
    }
}

@Command(name = 'subcmd2')
class SubCmd2 implements Callable<Object> { // (2)

    @Override
    Object call() throws Exception {
        'Hi Sub Command 2!'
    }
}
A top-level command with subcommands
import io.micronaut.configuration.picocli.PicocliRunner
import picocli.CommandLine.Command
import java.util.concurrent.Callable

@Command(name = "topcmd", subcommands = [SubCmd1::class, SubCmd2::class]) // (1)
class TopCommand : Callable<Any> { // (2)

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            PicocliRunner.execute(TopCommand::class.java, *args) // (3)
        }
    }

    @Throws(Exception::class)
    override fun call(): Any {
        return "Hi Top Command!"
    }
}

@Command(name = "subcmd1")
internal class SubCmd1 : Callable<Any> { // (2)

    @Throws(Exception::class)
    override fun call(): Any {
        return "Hi Sub Command 1!"
    }
}

@Command(name = "subcmd2")
internal class SubCmd2 : Callable<Any> { // (2)

    @Throws(Exception::class)
    override fun call(): Any {
        return "Hi Sub Command 2!"
    }
}
1 The top-level command has two subcommands, SubCmd1 and SubCmd2.
2 Let all commands in the hierarchy implement Runnable or Callable.
3 Start the application with PicocliRunner. This creates an ApplicationContext that instantiates the commands and performs the dependency injection.

8 Customizing Picocli

Occasionally you may want to set parser options or otherwise customize picocli behavior. This can easily be done via the setter methods on the picocli CommandLine object, but the PicocliRunner does not expose that object.

In such cases, you may want to invoke picocli directly instead of using the PicocliRunner. The code below demonstrates how to do this:

Example of customizing the picocli parser before invoking a command
import io.micronaut.configuration.picocli.MicronautFactory;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import picocli.CommandLine;
import picocli.CommandLine.*;
import java.util.concurrent.Callable;

@Command(name = "configuration-example")
public class ConfigDemo implements Callable<Object> {
    private static int execute(Class<?> clazz, String[] args) {
        try (ApplicationContext context = ApplicationContext.builder(
            clazz, Environment.CLI).start()) { // (1)

            return new CommandLine(clazz, new MicronautFactory(context)). // (2)
                setCaseInsensitiveEnumValuesAllowed(true). // (3)
                setUsageHelpAutoWidth(true). // (4)
                execute(args); // (5)
        }
    }

    public static void main(String[] args) {
        int exitCode = execute(ConfigDemo.class, args);
        System.exit(exitCode); // (6)
    }

    @Override
    public Object call() {
        return "Hi!";
    }
}
Example of customizing the picocli parser before invoking a command
import io.micronaut.configuration.picocli.MicronautFactory
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.Environment
import picocli.CommandLine
import picocli.CommandLine.Command
import java.util.concurrent.Callable

@Command(name = 'configuration-example')
class ConfigDemo implements Callable<Object> {
    private static int execute(Class<?> clazz, String[] args) {
        try (ApplicationContext context = ApplicationContext.builder(
                clazz, Environment.CLI).start()) { // (1)

            new CommandLine(clazz, new MicronautFactory(context)). // (2)
                    setCaseInsensitiveEnumValuesAllowed(true). // (3)
                    setUsageHelpAutoWidth(true). // (4)
                    execute(args) // (5)
        }
    }

    static void main(String[] args) {
        int exitCode = execute(ConfigDemo.class, args)
        System.exit(exitCode) // (6)
    }

    @Override
    Object call() {
        'Hi!'
    }
}
Example of customizing the picocli parser before invoking a command
import io.micronaut.configuration.picocli.MicronautFactory
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.Environment
import picocli.CommandLine
import picocli.CommandLine.Command
import java.util.concurrent.Callable
import kotlin.system.exitProcess

@Command(name = "configuration-example")
class ConfigDemo : Callable<Any> {

    companion object {
        private fun execute(clazz: Class<*>, args: Array<String>): Int {
            ApplicationContext.builder(clazz, Environment.CLI).start().use { context ->  // (1)

                return CommandLine(clazz, MicronautFactory(context)). // (2)
                setCaseInsensitiveEnumValuesAllowed(true). // (3)
                setUsageHelpAutoWidth(true). // (4)
                execute(*args) // (5)
            }
        }

        @JvmStatic
        fun main(args: Array<String>) {
            val exitCode = execute(ConfigDemo::class.java, args)
            exitProcess(exitCode) // (6)
        }
    }

    override fun call(): Any {
        return "Hi!"
    }
}
1 Instantiate a new ApplicationContext for the CLI environment, in a try-with-resources statements, so that the context is automatically closed before the method returns.
2 Pass a MicronautFactory with the application context to the picocli CommandLine constructor. This enables dependencies to be injected into the command and subcommands.
3 An example of configuring the picocli command line parser.
4 An example of configuring the picocli usage help message.
5 Execute the command and return the result (this closes the application context).
6 Optionally call System.exit with the returned exit code.

9 Repository

You can find the source code of this project in this repository: