picocli

A modern, feature-rich Java CLI library. Supports annotation-based parsing, strong typing, subcommands, and automatically generated colorful help messages.

javaclicommand-lineannotationsgraalvm

GitHub Overview

remkop/picocli

Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.

Stars5,156
Watchers43
Forks440
Created:February 1, 2017
Language:Java
License:Apache License 2.0

Topics

annotationsansiansi-colorsargument-parsingautocompletebash-completionclicli-frameworkcommand-linecommand-line-parsercommandlinecompletionexecutablegitgraalvmjavanative-imageoptions-parsingparsersubcommands

Star History

remkop/picocli Star History
Data as of: 7/25/2025, 02:05 AM

Framework

picocli

Overview

picocli is a modern Java framework for building powerful, user-friendly, GraalVM-enabled command line applications with ease. With the concept of "a mighty tiny command line interface," it provides rich functionality including color output, autocompletion, and subcommands through an annotation-based declarative approach. Composed of a single source file, it can be included directly as source to avoid dependencies, and is usable not only from Java but also from Groovy, Kotlin, and Scala.

Why is it the most popular in Java:

  • Complete GraalVM native image support: Achieves fast startup and low memory usage
  • Annotation-driven concise syntax: Rich functionality with minimal boilerplate
  • Comprehensive feature set: Supports POSIX, GNU, and MS-DOS style syntax
  • Strong type safety: Automatic type conversion and validation leveraging Java's type system

Detailed Description

History and Development

picocli is a Java CLI framework developed by Remko Popma, characterized by its innovative design using an annotation-based declarative approach. Unlike traditional programmatic APIs, it enables more readable and maintainable code by defining command-line structures using @Command and @Option annotations. Development continues actively, with complete support for the latest Java features and GraalVM ecosystem.

Position in the Ecosystem

picocli holds a special position in the Java ecosystem for the following reasons:

  • Most modern Java CLI library: Pioneer in annotation-driven and GraalVM support
  • Migration destination from JCommander: Provides more intuitive and type-safe approach
  • Cloud-native era compatibility: Optimal for cloud-native with fast startup and low resource consumption
  • Enterprise-grade functionality: Rich adoption track record in large-scale applications

Latest Trends (2024-2025)

v4.7.8 (July 2025 Latest Stable Version)

  • Continuous improvements: Stable release cycle with semantic versioning
  • Enhanced annotation processor: Improved accuracy of compile-time error checking
  • Automatic GraalVM configuration generation: Automatic generation of configuration files under META-INF/native-image
  • Performance optimizations: Further improvements in startup time and memory usage

v4.7.7 (2024 Stable Version)

  • ArgGroup improvements: Fixed duplicate option issues when combined with Mixins
  • HelpCommand enhancements: exitCodeOnUsageHelp support with Callable implementation
  • Execution strategy improvements: Prioritize call method when implementing both Callable and Runnable

Major Technical Innovations

  • Annotation processor: Compile-time type checking and automatic configuration file generation
  • Tracing API: Programmatic trace level setting
  • Variable interpolation: Dynamic resolution of system properties, environment variables, and resource bundle keys
  • Argument groups: Support for mutually exclusive/dependent options

Importance in the Cloud-Native Era (2024-2025)

  • Container optimization: Dramatically reduce container size with GraalVM native images
  • Serverless compatibility: Perfect for AWS Lambda and Google Cloud Functions with fast startup
  • Kubernetes integration: Utilization in lightweight init containers and sidecar patterns
  • CI/CD pipelines: High-performance execution in build tools and deployment scripts

Key Features

Core Functionality

  • Annotation-driven: Declarative definition using @Command, @Option, @Parameters
  • Rich syntax support: POSIX, GNU, and MS-DOS style command-line syntax
  • Automatic type conversion: Leverages Java type system for automatic argument conversion
  • Comprehensive validation: Support for arity, choices, and custom validation
  • Subcommands: Hierarchical command structure and dynamic subcommand discovery

Advanced Features

  • Argument groups: Grouping of mutually exclusive/dependent options
  • Mixin support: Reuse of common options and modular design
  • Custom help: Detailed customization of ANSI colored help messages
  • Auto-completion: Tab completion script generation for Bash, Zsh, Fish
  • Internationalization: Multi-language support through resource bundles

GraalVM Integration

  • Native image support: Fast startup (millisecond level) and low memory usage
  • Automatic configuration generation: Automatic reflection configuration generation via annotation processor
  • Cross-platform: Executable file generation for Windows, Linux, macOS
  • Jansi integration: Color output support on Windows

Latest Features and Updates

Annotation Processor (picocli-codegen)

  • Compile-time error checking: Early detection of invalid annotations and attributes
  • Automatic GraalVM configuration generation: Automatic creation of reflect-config.json, resource-config.json, proxy-config.json
  • Project-specific configuration: Namespace separation of configuration files via -Aproject option

Tracing and Debugging

  • Programmatic tracing: Dynamic control via CommandLine.tracer().setLevel()
  • Detailed error messages: Detailed information on type conversion errors and validation failures
  • Runtime diagnostics: Runtime validation and reporting of annotation configuration

Performance Improvements

  • Startup time optimization: Acceleration via picocli.disable.closures system property
  • char[] as single-value type: Type system improvement in 4.7.0
  • Execution strategy optimization: Efficient selection between Callable and Runnable

Pros and Cons

Pros

Functionality

  • Strong type safety: Automatic type conversion leveraging Java type system
  • Rich functionality: Comprehensive support from basic to advanced features
  • Excellent developer experience: Intuitive development through annotation-driven approach
  • Complete GraalVM support: High performance with native images

Quality

  • High reliability: Rich track record in enterprise environments
  • Active maintenance: Continuous improvement and support for latest technologies
  • Comprehensive documentation: Detailed documentation and abundant samples
  • Strong test support: Easy integration with unit tests

Cons

Learning Cost Aspects

  • Annotation complexity: Learning cost when using advanced features
  • GraalVM-specific constraints: Need to understand limitations in native images

Performance Aspects

  • JVM startup overhead: Startup time in traditional Java execution (resolved with native images)
  • Memory usage: Slight memory overhead due to rich functionality

Key Links

Official Resources

Documentation

Usage Examples

Basic Usage Example

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;

@Command(name = "example", mixinStandardHelpOptions = true, version = "Picocli example 4.0")
public class Example implements Runnable {

    @Option(names = { "-v", "--verbose" },
      description = "Verbose mode. Helpful for troubleshooting. Multiple -v options increase the verbosity.")
    private boolean[] verbose = new boolean[0];

    @Parameters(arity = "1..*", paramLabel = "FILE", description = "File(s) to process.")
    private File[] inputFiles;

    public void run() {
        if (verbose.length > 0) {
            System.out.println(inputFiles.length + " files to process...");
        }
        if (verbose.length > 1) {
            for (File f : inputFiles) {
                System.out.println(f.getAbsolutePath());
            }
        }
    }

    public static void main(String[] args) {
        // By implementing Runnable or Callable, parsing, error handling and handling user
        // requests for usage help or version help can be done with one line of code.

        int exitCode = new CommandLine(new Example()).execute(args);
        System.exit(exitCode);
    }
}
# Usage example
java Example file1.txt file2.txt -v
# Output: 2 files to process...

java Example file1.txt --verbose --verbose
# Output: 
# 1 files to process...
# /path/to/file1.txt

Checksum Application

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.math.BigInteger;
import java.util.concurrent.Callable;

@Command(name = "checksum", mixinStandardHelpOptions = true, version = "Checksum 4.0",
          description = "Prints the checksum (SHA-1 by default) of a file to STDOUT.")
public class CheckSum implements Callable<Integer> {

    @Parameters(index = "0", description = "The file whose checksum to calculate.")
    private File file;

    @Option(names = {"-a", "--algorithm"}, description = "MD5, SHA-1, SHA-256, ...")
    private String algorithm = "SHA-1";

    @Override
    public Integer call() throws Exception {
        byte[] fileContents = Files.readAllBytes(file.toPath());
        byte[] digest = MessageDigest.getInstance(algorithm).digest(fileContents);
        System.out.printf("%0" + (digest.length*2) + "x%n", new BigInteger(1, digest));
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new CheckSum()).execute(args);
        System.exit(exitCode);
    }
}
# Usage example
java CheckSum myfile.txt --algorithm=MD5
# Output: a1b2c3d4e5f6789... (MD5 hash)

java CheckSum myfile.txt -a SHA-256
# Output: 9f86d0818cf7... (SHA-256 hash)

Detailed Option and Parameter Definition

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.util.List;

@Command(name = "encrypt", 
         description = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
         version = "Encrypt version 1.0",
         footer = "Copyright (c) 2017",
         exitCodeListHeading = "Exit Codes:%n",
         exitCodeList = { 
             " 0:Successful program execution.",
             "64:Invalid input: an unknown option or invalid parameter was specified.",
             "70:Execution exception: an exception occurred while executing the business logic."
         })
public class Encrypt {
    @Parameters(paramLabel = "FILE", description = "Any number of input files")
    private List<File> files = new ArrayList<File>();

    @Option(names = { "-o", "--out" }, description = "Output file (default: print to console)")
    private File outputFile;

    @Option(names = { "-v", "--verbose"}, 
            description = "Verbose mode. Helpful for troubleshooting. Multiple -v options increase the verbosity.")
    private boolean[] verbose;

    @Option(names = { "-c", "--cipher" }, 
            description = "Encryption algorithm: ${COMPLETION-CANDIDATES}",
            completionCandidates = CipherCandidates.class)
    private String cipher = "AES";

    @Option(names = { "-p", "--password" }, 
            description = "Encryption password",
            interactive = true,
            arity = "0..1")
    private char[] password;

    static class CipherCandidates extends ArrayList<String> {
        CipherCandidates() { super(Arrays.asList("AES", "DES", "RSA")); }
    }
}

Subcommand Implementation

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

@Command(name = "git", 
         mixinStandardHelpOptions = true,
         version = "subcommand demo - picocli 4.0",
         subcommands = {GitCommit.class, GitStatus.class, GitPush.class},
         description = "Git is a fast, scalable, distributed revision control system")
public class Git implements Runnable {

    @Option(names = "--git-dir", description = "Set the path to the repository.")
    private File gitDir;

    public void run() {
        System.out.println("Git command executed");
    }

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

@Command(name = "commit", description = "Record changes to the repository")
class GitCommit implements Runnable {
    @Option(names = {"-m", "--message"}, description = "Use the given message as the commit message")
    private String message;

    @Option(names = {"-a", "--all"}, description = "Automatically stage files that have been modified and deleted")
    private boolean all;

    public void run() {
        System.out.println("Committing changes...");
        if (message != null) {
            System.out.println("Commit message: " + message);
        }
        if (all) {
            System.out.println("Staging all modified files");
        }
    }
}

@Command(name = "status", description = "Show the working tree status")
class GitStatus implements Runnable {
    @Option(names = {"-s", "--short"}, description = "Give the output in the short-format")
    private boolean shortFormat;

    public void run() {
        System.out.println("Repository status:");
        if (shortFormat) {
            System.out.println("(short format)");
        }
    }
}

@Command(name = "push", description = "Update remote refs along with associated objects")
class GitPush implements Runnable {
    @Parameters(index = "0", description = "The remote repository")
    private String remote = "origin";

    @Parameters(index = "1", description = "The branch to push")
    private String branch = "main";

    @Option(names = {"-f", "--force"}, description = "Force push")
    private boolean force;

    public void run() {
        System.out.println("Pushing to " + remote + "/" + branch);
        if (force) {
            System.out.println("Force push enabled");
        }
    }
}

Argument Groups (Mutually Exclusive Options)

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.ArgGroup;

@Command(name = "exclusive", description = "Demonstrates mutually exclusive options")
public class ExclusiveOptions implements Runnable {

    @ArgGroup(exclusive = true, multiplicity = "1")
    Exclusive exclusive;

    static class Exclusive {
        @Option(names = "-a", description = "Option A") 
        boolean a;
        
        @Option(names = "-b", description = "Option B") 
        boolean b;
        
        @Option(names = "-c", description = "Option C") 
        boolean c;
    }

    @ArgGroup(exclusive = false, multiplicity = "1")
    Dependent dependent;

    static class Dependent {
        @Option(names = "--user", required = true, description = "User name") 
        String user;
        
        @Option(names = "--password", required = true, description = "Password") 
        String password;
    }

    public void run() {
        System.out.println("Exclusive option selected");
        System.out.println("User: " + dependent.user);
    }

    public static void main(String[] args) {
        new CommandLine(new ExclusiveOptions()).execute(args);
    }
}
# Usage example (success)
java ExclusiveOptions -a --user=john --password=secret

# Usage example (error: mutually exclusive options)  
java ExclusiveOptions -a -b --user=john --password=secret
# Error: Error: -a, -b are mutually exclusive (specify only one)

Common Option Reuse with Mixins

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Mixin;

// Mixin defining common options
class CommonOptions {
    @Option(names = {"-v", "--verbose"}, description = "Verbose output")
    boolean verbose;

    @Option(names = {"-o", "--output"}, description = "Output file")
    File outputFile;
}

// Database-related common options
class DatabaseOptions {
    @Option(names = {"--host"}, description = "Database host", defaultValue = "localhost")
    String host;

    @Option(names = {"--port"}, description = "Database port", defaultValue = "5432")
    int port;

    @Option(names = {"--database"}, description = "Database name")
    String database;
}

@Command(name = "backup", description = "Backup database")
class BackupCommand implements Runnable {
    @Mixin CommonOptions commonOptions;
    @Mixin DatabaseOptions dbOptions;

    @Option(names = {"--compress"}, description = "Compress backup file")
    boolean compress;

    public void run() {
        if (commonOptions.verbose) {
            System.out.println("Backing up database " + dbOptions.database);
            System.out.println("Host: " + dbOptions.host + ":" + dbOptions.port);
        }
        
        System.out.println("Creating backup...");
        if (compress) {
            System.out.println("Compressing backup file");
        }
        
        if (commonOptions.outputFile != null) {
            System.out.println("Output file: " + commonOptions.outputFile);
        }
    }
}

@Command(name = "restore", description = "Restore database")
class RestoreCommand implements Runnable {
    @Mixin CommonOptions commonOptions;
    @Mixin DatabaseOptions dbOptions;

    @Option(names = {"--backup-file"}, required = true, description = "Backup file to restore")
    File backupFile;

    public void run() {
        if (commonOptions.verbose) {
            System.out.println("Restoring database " + dbOptions.database);
        }
        
        System.out.println("Restoring from: " + backupFile);
    }
}

Advanced Validation and Custom Type Conversion

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ITypeConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.regex.Pattern;

// Custom type converter
class LocalDateConverter implements ITypeConverter<LocalDate> {
    public LocalDate convert(String value) throws Exception {
        return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

// Custom validator
class EmailValidator implements IParameterConsumer {
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");

    public void consumeParameters(Stack<String> args, ArgSpec argSpec, CommandSpec commandSpec) {
        String email = args.pop();
        if (!EMAIL_PATTERN.matcher(email).matches()) {
            throw new ParameterException(commandSpec.commandLine(), 
                "Invalid email format: " + email);
        }
        argSpec.setValue(email);
    }
}

@Command(name = "register", description = "User registration command")
public class RegisterCommand implements Runnable {

    @Option(names = {"--email"}, 
            description = "User email address",
            parameterConsumer = EmailValidator.class,
            required = true)
    private String email;

    @Option(names = {"--birthdate"}, 
            description = "Birth date (YYYY-MM-DD)",
            converter = LocalDateConverter.class)
    private LocalDate birthDate;

    @Option(names = {"--age"}, 
            description = "Age (must be between 13 and 120)")
    @Range(min = 13, max = 120)
    private int age;

    @Parameters(index = "0",
                description = "Username (3-20 characters, alphanumeric only)")
    @Pattern(regexp = "^[a-zA-Z0-9]{3,20}$", 
             message = "Username must be 3-20 alphanumeric characters")
    private String username;

    public void run() {
        System.out.println("Registering user: " + username);
        System.out.println("Email: " + email);
        System.out.println("Age: " + age);
        if (birthDate != null) {
            System.out.println("Birth date: " + birthDate);
        }
    }

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

GraalVM Native Image Configuration

// CheckSum.java - GraalVM compatible sample application
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.concurrent.Callable;

@Command(name = "checksum", 
         mixinStandardHelpOptions = true, 
         version = "checksum 4.0",
         description = "Calculate file checksum")
public class CheckSum implements Callable<Integer> {

    @Parameters(index = "0", description = "The file whose checksum to calculate.")
    private File file;

    @Option(names = {"-a", "--algorithm"}, 
            description = "Hash algorithm: MD5, SHA-1, SHA-256, etc.",
            defaultValue = "SHA-1")
    private String algorithm;

    @Override
    public Integer call() throws Exception {
        byte[] fileContents = Files.readAllBytes(file.toPath());
        byte[] digest = MessageDigest.getInstance(algorithm).digest(fileContents);
        
        StringBuilder result = new StringBuilder();
        for (byte b : digest) {
            result.append(String.format("%02x", b));
        }
        System.out.println(result.toString());
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new CheckSum()).execute(args);
        System.exit(exitCode);
    }
}
<!-- pom.xml - Maven annotation processor configuration -->
<project>
    <properties>
        <picocli.version>4.7.8-SNAPSHOT</picocli.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>info.picocli</groupId>
            <artifactId>picocli</artifactId>
            <version>${picocli.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>info.picocli</groupId>
                            <artifactId>picocli-codegen</artifactId>
                            <version>${picocli.version}</version>
                        </path>
                    </annotationProcessorPaths>
                    <compilerArgs>
                        <arg>-Aproject=checksum</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
// build.gradle - Gradle annotation processor configuration
plugins {
    id 'java'
    id 'com.palantir.graal' version '0.10.0'
}

dependencies {
    implementation 'info.picocli:picocli:4.7.8-SNAPSHOT'
    annotationProcessor 'info.picocli:picocli-codegen:4.7.8-SNAPSHOT'
}

compileJava {
    options.compilerArgs += ['-Aproject=checksum']
}

graal {
    outputName 'checksum'
    mainClass 'CheckSum'
    option '--static'
    option '--no-server'
}
# Building GraalVM Native Image
# 1. Compile application (annotation processor automatically generates config files)
javac -cp picocli-4.7.8-SNAPSHOT.jar CheckSum.java

# 2. Create JAR file
jar -cfe checksum.jar CheckSum *.class

# 3. Verify JAR contents (check auto-generated config files)
jar -tf checksum.jar
# META-INF/native-image/picocli-generated/
# META-INF/native-image/picocli-generated/proxy-config.json
# META-INF/native-image/picocli-generated/reflect-config.json
# META-INF/native-image/picocli-generated/resource-config.json

# 4. Build native image
native-image -cp picocli-4.7.8-SNAPSHOT.jar --static -jar checksum.jar

# 5. Performance comparison
time java -cp checksum.jar CheckSum myfile.txt
# real    0m0.500s  (JVM startup overhead)

time ./checksum myfile.txt  
# real    0m0.005s  (native image fast startup)

Internationalization and Localization

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.ResourceBundle;

@Command(name = "i18n-example", 
         resourceBundle = "messages",
         description = "%command.description")
public class I18nExample implements Runnable {

    @Option(names = {"-l", "--language"}, 
            description = "%option.language.description")
    private String language = "en";

    @Option(names = {"-f", "--file"}, 
            description = "%option.file.description")
    private String filename;

    public void run() {
        ResourceBundle bundle = ResourceBundle.getBundle("messages", 
            java.util.Locale.forLanguageTag(language));
        
        System.out.println(bundle.getString("greeting"));
        if (filename != null) {
            System.out.println(bundle.getString("processing.file") + ": " + filename);
        }
    }

    public static void main(String[] args) {
        new CommandLine(new I18nExample()).execute(args);
    }
}
# messages_en.properties
command.description=Internationalization example
option.language.description=Language code (en, ja, de)
option.file.description=File to process
greeting=Hello, World!
processing.file=Processing file

# messages_ja.properties  
command.description=国際化のサンプル
option.language.description=言語コード (en, ja, de)
option.file.description=処理するファイル
greeting=こんにちは、世界!
processing.file=ファイルを処理中

# messages_de.properties
command.description=Internationalisierungsbeispiel
option.language.description=Sprachcode (en, ja, de)
option.file.description=Zu verarbeitende Datei
greeting=Hallo, Welt!
processing.file=Verarbeite Datei

Test Integration

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import picocli.CommandLine;
import java.io.File;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*;

public class CheckSumTest {

    @Test
    public void testBasicUsage(@TempDir Path tempDir) throws Exception {
        // Create test file
        File testFile = tempDir.resolve("test.txt").toFile();
        Files.write(testFile.toPath(), "Hello, World!".getBytes());

        // Execute command
        CheckSum checkSum = new CheckSum();
        CommandLine cmd = new CommandLine(checkSum);
        
        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        
        int exitCode = cmd.execute(testFile.getAbsolutePath());
        
        // Verify results
        assertEquals(0, exitCode);
        String output = sw.toString().trim();
        assertFalse(output.isEmpty());
        assertEquals(40, output.length()); // SHA-1 is 40 characters
    }

    @Test
    public void testInvalidFile() {
        CheckSum checkSum = new CheckSum();
        CommandLine cmd = new CommandLine(checkSum);
        
        StringWriter sw = new StringWriter();
        cmd.setErr(new PrintWriter(sw));
        
        int exitCode = cmd.execute("nonexistent.txt");
        
        // Verify error is handled properly
        assertNotEquals(0, exitCode);
        String errorOutput = sw.toString();
        assertTrue(errorOutput.contains("nonexistent.txt"));
    }

    @Test
    public void testVersionOption() {
        CheckSum checkSum = new CheckSum();
        CommandLine cmd = new CommandLine(checkSum);
        
        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        
        int exitCode = cmd.execute("--version");
        
        assertEquals(0, exitCode);
        String output = sw.toString();
        assertTrue(output.contains("checksum 4.0"));
    }

    @Test
    public void testHelpOption() {
        CheckSum checkSum = new CheckSum();
        CommandLine cmd = new CommandLine(checkSum);
        
        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        
        int exitCode = cmd.execute("--help");
        
        assertEquals(0, exitCode);
        String output = sw.toString();
        assertTrue(output.contains("Usage:"));
        assertTrue(output.contains("algorithm"));
    }
}

Programmatic API

import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.Model.PositionalParamSpec;

public class ProgrammaticExample {
    
    public static void main(String[] args) {
        // Build command specification programmatically
        CommandSpec spec = CommandSpec.create()
            .name("dynamic-command")
            .version("1.0")
            .addOption(OptionSpec.builder("-v", "--verbose")
                .description("Enable verbose output")
                .type(boolean.class)
                .build())
            .addOption(OptionSpec.builder("-f", "--file")
                .description("Input file")
                .type(File.class)
                .required(true)
                .build())
            .addPositional(PositionalParamSpec.builder()
                .index("0..*")
                .description("Additional arguments")
                .type(String[].class)
                .build());

        // Define runtime processing
        spec.addSubcommand("process", CommandSpec.create()
            .addOption(OptionSpec.builder("--format")
                .description("Output format")
                .type(String.class)
                .defaultValue("json")
                .build()));

        CommandLine cmd = new CommandLine(spec);
        
        // Custom ExecutionStrategy
        cmd.setExecutionStrategy(new CommandLine.IExecutionStrategy() {
            public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException {
                System.out.println("Custom execution strategy");
                
                // Get parsed values
                boolean verbose = parseResult.matchedOptionValue("-v", false);
                File file = parseResult.matchedOptionValue("-f", null);
                
                if (verbose) {
                    System.out.println("Processing file: " + file);
                }
                
                return 0;
            }
        });

        // Execute
        int exitCode = cmd.execute(args);
        System.exit(exitCode);
    }
}

As demonstrated, picocli is a comprehensive framework that supports powerful CLI application development in Java, from basic usage to advanced features. Its integration with GraalVM is particularly noteworthy, enabling fast startup and low memory usage that was difficult to achieve with traditional Java applications.