$ mn create-cli-app my-app
Micronaut Picocli Configuration
Provides integration between Micronaut and Picocli
Version: 5.6.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:
or with Micronaut 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:
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!");
}
}
}
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!")
}
}
}
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:
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);
}
}
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)
}
}
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:
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:
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!";
}
}
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!'
}
}
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:
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!";
}
}
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!'
}
}
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: