Picocli is a small Java library that makes it easy to build command-line tools. You write plain Java classes, add a few annotations, and Picocli handles parsing, help text, and error messages for you.

Why build a CLI tool?

Many useful tools run in the terminal:

  • Build scripts
  • Data migration utilities
  • Dev helpers (rename files, check configs, seed databases)
  • Internal admin tools

You can parse args manually with String[] args, but that gets messy quickly when you need flags, subcommands, defaults, and --help.

Picocli solves that. It is popular, well documented, and works nicely with Java projects.

What is Picocli?

Picocli is an open-source Java framework for building command-line interfaces.

With Picocli you can:

  • Define options like --name Siva or -n Siva
  • Define positional arguments like a file path
  • Create subcommands like git commit or mytool export
  • Auto-generate help and version output
  • Show clear errors when the user types invalid input

You describe your CLI in Java. Picocli does the parsing.

A simple example: a greeting CLI

Let us build a tiny tool called greet that says hello to someone.

1. Add the dependency

Maven:

<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>4.7.6</version>
</dependency>

Gradle:

implementation 'info.picocli:picocli:4.7.6'

2. Create the command class

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

@Command(
    name = "greet",
    mixinStandardHelpOptions = true,
    version = "greet 1.0",
    description = "Prints a greeting."
)
public class GreetCommand implements Runnable {

    @Option(names = {"-n", "--name"}, description = "Name to greet", defaultValue = "World")
    String name;

    @Option(names = {"-u", "--uppercase"}, description = "Print in uppercase")
    boolean uppercase;

    @Parameters(index = "0", arity = "0..1", description = "Optional extra message")
    String message;

    @Override
    public void run() {
        String text = "Hello, " + name;
        if (message != null && !message.isBlank()) {
            text += " — " + message;
        }
        if (uppercase) {
            text = text.toUpperCase();
        }
        System.out.println(text);
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new GreetCommand()).execute(args);
        System.exit(exitCode);
    }
}

3. Run it

java GreetCommand
# Hello, World

java GreetCommand --name Siva
# Hello, Siva

java GreetCommand -n Siva -u "welcome to the blog"
# HELLO, SIVA — WELCOME TO THE BLOG

java GreetCommand --help
# Shows usage, options, and description

That is a working CLI with help text and almost no parsing code.

How Picocli maps input to your class

User types Picocli sets
--name Siva name = "Siva"
-u or --uppercase uppercase = true
welcome (positional) message = "welcome"
--help prints usage and exits
wrong flag clear error message

Picocli reads annotations on fields and methods, then fills them from args.

Common annotations

@Option

For named flags and values:

@Option(names = {"-v", "--verbose"}, description = "Show more output")
boolean verbose;

@Option(names = "--count", defaultValue = "1", description = "Repeat count")
int count;

@Parameters

For positional arguments (no flag name):

@Parameters(index = "0", description = "Input file")
Path inputFile;

@Command

On the main class or subcommand:

@Command(name = "mytool", description = "Does useful work")

Useful settings:

  • mixinStandardHelpOptions = true — adds --help and --version
  • subcommands = { ExportCommand.class, ImportCommand.class } — nested commands

Subcommands: one tool, many actions

Real CLIs often have subcommands. Think of docker run, docker ps, or kubectl get.

Example structure:

@Command(
    name = "files",
    subcommands = {
        ListCommand.class,
        CopyCommand.class
    }
)
public class FilesApp implements Runnable {
    @Override
    public void run() {
        CommandLine.usage(this, System.out);
    }

    public static void main(String[] args) {
        System.exit(new CommandLine(new FilesApp()).execute(args));
    }
}

@Command(name = "list", description = "List files in a folder")
class ListCommand implements Runnable {
    @Parameters(index = "0", description = "Directory")
    File dir;

    @Override
    public void run() {
        File[] files = dir.listFiles();
        if (files == null) {
            System.err.println("Not a directory: " + dir);
            return;
        }
        for (File f : files) {
            System.out.println(f.getName());
        }
    }
}

Usage:

java FilesApp list /tmp

Picocli routes list to ListCommand automatically.

Validation and user-friendly errors

You can check input inside run():

@Override
public void run() {
    if (count < 1) {
        throw new CommandLine.ParameterException(
            new CommandLine(this),
            "Count must be at least 1"
        );
    }
    // ...
}

Or use a separate validation method with @Command — Picocli also supports ITypeConverter for custom types (for example, parsing a string into an enum or Path).

Good error messages matter. A CLI that says “invalid value for option ‘–port’” is much easier to use than a stack trace.

Exit codes

CommandLine.execute(args) returns an exit code:

  • 0 — success
  • non-zero — failure

Shell scripts and CI pipelines use this to know if a command passed or failed.

int exitCode = new CommandLine(new GreetCommand()).execute(args);
System.exit(exitCode);

Return errors from your logic by throwing CommandLine.ExecutionException or using Picocli’s built-in exit code mapping.

Package as a runnable JAR

For a real tool, ship a fat JAR (all dependencies included) with a main class.

Maven (shade plugin snippet):

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <goals><goal>shade</goal></goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>GreetCommand</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Then:

java -jar greet.jar --name Siva

You can also use jpackage (JDK 14+) to build a native installer or executable for Windows, macOS, or Linux.

Picocli vs manual parsing

Approach Pros Cons
Manual args parsing No dependency Hard to maintain, poor help text
Picocli Clean code, help, subcommands, validation Small dependency

For anything beyond one or two flags, Picocli is worth it.

Tips for good CLI design

  1. Keep commands focused — one job per subcommand.
  2. Use sensible defaultsgreet without --name should still work.
  3. Write clear --help text — users read help first.
  4. Print errors to stderr — use System.err for errors, stdout for normal output.
  5. Return proper exit codes0 on success, non-zero on failure.
  6. Name flags consistently — short (-v) and long (--verbose) forms together.

When to pick Picocli

Picocli is a great fit if you:

  • Build Java CLI tools for your team or open source
  • Want annotation-based, readable command definitions
  • Need subcommands, tab completion, and good help output
  • Prefer staying in Java without switching to Python or Go

For very large CLIs, also look at Picocli’s support for completion scripts (bash/zsh) and ANSI colors for richer terminal output.

Quick recap

  1. Add the Picocli dependency.
  2. Create a class with @Command, @Option, and @Parameters.
  3. Implement Runnable and put your logic in run().
  4. Call new CommandLine(new YourCommand()).execute(args) from main.
  5. Package as a JAR and share your tool.

In a few dozen lines of Java, you get a proper CLI with --help, options, and clean parsing — without writing a parser yourself.

Further reading