picocli

モダンで機能豊富なJava CLIライブラリ。アノテーションベースのパーシング、強い型付け、サブコマンド、カラフルなヘルプメッセージの自動生成をサポート。

javaclicommand-lineannotationsgraalvm

GitHub概要

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.

スター5,156
ウォッチ43
フォーク440
作成日:2017年2月1日
言語:Java
ライセンス:Apache License 2.0

トピックス

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

スター履歴

remkop/picocli Star History
データ取得日時: 2025/7/25 02:05

フレームワーク

picocli

概要

picocliは、強力でユーザーフレンドリーなGraalVM対応コマンドラインアプリケーションを簡単に構築するモダンなJavaフレームワークです。「a mighty tiny command line interface」をコンセプトに、アノテーションベースの宣言的アプローチで、カラー出力、自動補完、サブコマンドなどの豊富な機能を提供します。単一のソースファイルで構成されるため、依存関係を回避してソースとして直接インクルードすることも可能で、JavaだけでなくGroovy、Kotlin、Scalaからも利用できます。

なぜJavaで最も人気なのか:

  • GraalVMネイティブイメージ完全対応: 高速起動と低メモリ使用量を実現
  • アノテーション駆動の簡潔な記述: 最小限のボイラープレートで豊富な機能
  • 包括的な機能セット: POSIX、GNU、MS-DOSスタイルの構文をサポート
  • 強力な型安全性: Java型システムを活用した自動型変換とバリデーション

詳細説明

歴史と発展

picocliは、Remko Popma氏によって開発されたJava CLIフレームワークで、アノテーションベースの宣言的アプローチによる革新的な設計が特徴です。従来のプログラマティックなAPIとは異なり、@Commandや@Optionアノテーションを使用してコマンドライン構造を定義することで、より読みやすく保守しやすいコードを実現しています。現在もアクティブに開発が続けられており、最新のJava機能とGraalVMエコシステムに完全対応しています。

エコシステムでの位置づけ

Javaエコシステムにおいて、picocliは以下の理由で特別な地位を占めています:

  • 最もモダンなJava CLIライブラリ: アノテーション駆動とGraalVM対応の先駆者
  • JCommanderからの移行先: より直感的で型安全なアプローチを提供
  • マイクロサービス時代への対応: 高速起動と低リソース消費でクラウドネイティブに最適
  • エンタープライズグレードの機能: 大規模アプリケーションでの豊富な採用実績

最新動向(2024-2025年)

v4.7.8(2025年7月最新安定版)

  • 継続的な改善: セマンティックバージョニングによる安定したリリースサイクル
  • アノテーションプロセッサの強化: コンパイル時エラーチェックの精度向上
  • GraalVM設定の自動生成: META-INF/native-image配下への自動設定ファイル生成
  • パフォーマンス最適化: 起動時間とメモリ使用量のさらなる改善

v4.7.7(2024年安定版)

  • ArgGroupの改善: Mixinとの組み合わせでの重複オプション問題を修正
  • HelpCommandの強化: Callable実装によるexitCodeOnUsageHelp対応
  • 実行戦略の改善: CallableとRunnableの両方を実装する場合にcallメソッドを優先

主要な技術革新

  • アノテーションプロセッサ: コンパイル時の型チェックと設定ファイル自動生成
  • トレーシングAPI: プログラマティックなトレースレベル設定
  • 変数補間: システムプロパティ、環境変数、リソースバンドルキーの動的解決
  • 引数グループ: 相互排他的/依存的オプションのサポート

クラウドネイティブ時代での重要性(2024-2025年)

  • コンテナ最適化: GraalVMネイティブイメージでコンテナサイズを大幅削減
  • サーバーレス対応: 高速起動によりAWS Lambda、Google Cloud Functionsに最適
  • Kubernetes統合: 軽量なinitコンテナやサイドカーパターンでの活用
  • CI/CDパイプライン: ビルドツールやデプロイメントスクリプトでの高パフォーマンス実行

主な特徴

コア機能

  • アノテーション駆動: @Command、@Option、@Parametersによる宣言的定義
  • 豊富な構文サポート: POSIX、GNU、MS-DOSスタイルのコマンドライン構文
  • 自動型変換: Java型システムを活用した引数の自動変換
  • 包括的バリデーション: アリティ、選択肢、カスタムバリデーションのサポート
  • サブコマンド: 階層的なコマンド構造と動的サブコマンド発見

高度な機能

  • 引数グループ: 相互排他的・依存的オプションのグループ化
  • Mixinサポート: 共通オプションの再利用とモジュラー設計
  • カスタムヘルプ: ANSI色付きヘルプメッセージの詳細カスタマイズ
  • 自動補完: Bash、Zsh、Fishでのタブ補完スクリプト生成
  • 国際化: リソースバンドルによる多言語対応

GraalVM統合

  • ネイティブイメージ対応: 高速起動(ミリ秒レベル)と低メモリ使用量
  • 自動設定生成: アノテーションプロセッサによるリフレクション設定の自動生成
  • クロスプラットフォーム: Windows、Linux、macOS向けの実行可能ファイル生成
  • Jansi統合: Windowsでのカラー出力サポート

最新機能とアップデート

アノテーションプロセッサ(picocli-codegen)

  • コンパイル時エラーチェック: 不正なアノテーションや属性の事前検出
  • GraalVM設定自動生成: reflect-config.json、resource-config.json、proxy-config.jsonの自動作成
  • プロジェクト固有設定: -Aprojectオプションによる設定ファイルの名前空間分離

トレーシングとデバッグ

  • プログラマティックトレーシング: CommandLine.tracer().setLevel()による動的制御
  • 詳細なエラーメッセージ: 型変換エラーやバリデーション失敗の詳細情報
  • 実行時診断: アノテーション設定の実行時検証とレポート

パフォーマンス改善

  • 起動時間最適化: picocli.disable.closuresシステムプロパティによる高速化
  • char[]の単一値型扱い: 4.7.0での型システム改善
  • 実行戦略の最適化: CallableとRunnableの効率的な選択

メリット・デメリット

メリット

機能面

  • 強力な型安全性: Java型システムを活用した自動型変換
  • 豊富な機能: 基本から高度な機能まで包括的にサポート
  • 優れた開発体験: アノテーション駆動による直感的な開発
  • GraalVM完全対応: ネイティブイメージでの高パフォーマンス

品質面

  • 高い信頼性: エンタープライズ環境での豊富な実績
  • アクティブなメンテナンス: 継続的な改善と最新技術への対応
  • 包括的なドキュメント: 詳細なドキュメントと豊富なサンプル
  • 強力なテストサポート: 単体テストとの統合が容易

デメリット

学習コストの側面

  • アノテーションの複雑さ: 高度な機能を使う際の学習コスト
  • GraalVM特有の制約: ネイティブイメージでの制限事項への理解が必要

パフォーマンス面

  • JVM起動オーバーヘッド: 従来のJava実行時の起動時間(ネイティブイメージで解決)
  • メモリ使用量: 機能豊富ゆえの若干のメモリオーバーヘッド

主要リンク

公式リソース

ドキュメント

書き方の例

基本的な使用例

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);
    }
}
# 実行例
java Example file1.txt file2.txt -v
# 出力: 2 files to process...

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

チェックサムアプリケーション

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);
    }
}
# 実行例
java CheckSum myfile.txt --algorithm=MD5
# 出力: a1b2c3d4e5f6789... (MD5ハッシュ)

java CheckSum myfile.txt -a SHA-256
# 出力: 9f86d0818cf7... (SHA-256ハッシュ)

オプションとパラメータの詳細定義

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")); }
    }
}

サブコマンドの実装

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");
        }
    }
}

引数グループ(相互排他的オプション)

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);
    }
}
# 実行例(成功)
java ExclusiveOptions -a --user=john --password=secret

# 実行例(エラー:相互排他的オプション)  
java ExclusiveOptions -a -b --user=john --password=secret
# エラー: Error: -a, -b are mutually exclusive (specify only one)

Mixinによる共通オプションの再利用

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

// 共通のオプションを定義するMixin
class CommonOptions {
    @Option(names = {"-v", "--verbose"}, description = "Verbose output")
    boolean verbose;

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

// データベース関連の共通オプション
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);
    }
}

高度なバリデーションとカスタム型変換

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;

// カスタム型変換器
class LocalDateConverter implements ITypeConverter<LocalDate> {
    public LocalDate convert(String value) throws Exception {
        return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

// カスタムバリデーター
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ネイティブイメージの設定

// CheckSum.java - GraalVM対応のサンプルアプリケーション
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でのアノテーションプロセッサ設定 -->
<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でのアノテーションプロセッサ設定
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'
}
# GraalVMネイティブイメージのビルド
# 1. アプリケーションをコンパイル(アノテーションプロセッサが設定ファイルを自動生成)
javac -cp picocli-4.7.8-SNAPSHOT.jar CheckSum.java

# 2. JARファイルを作成
jar -cfe checksum.jar CheckSum *.class

# 3. JARの内容確認(自動生成された設定ファイルを確認)
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. ネイティブイメージをビルド
native-image -cp picocli-4.7.8-SNAPSHOT.jar --static -jar checksum.jar

# 5. パフォーマンス比較
time java -cp checksum.jar CheckSum myfile.txt
# real    0m0.500s  (JVM起動オーバーヘッドあり)

time ./checksum myfile.txt  
# real    0m0.005s  (ネイティブイメージは高速起動)

国際化とローカライゼーション

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

テスト統合

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 {
        // テスト用ファイルを作成
        File testFile = tempDir.resolve("test.txt").toFile();
        Files.write(testFile.toPath(), "Hello, World!".getBytes());

        // コマンドを実行
        CheckSum checkSum = new CheckSum();
        CommandLine cmd = new CommandLine(checkSum);
        
        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        
        int exitCode = cmd.execute(testFile.getAbsolutePath());
        
        // 結果を検証
        assertEquals(0, exitCode);
        String output = sw.toString().trim();
        assertFalse(output.isEmpty());
        assertEquals(40, output.length()); // SHA-1は40文字
    }

    @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");
        
        // エラーが適切に処理されることを確認
        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"));
    }
}

プログラマティック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) {
        // プログラマティックにコマンド仕様を構築
        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());

        // 実行時の処理を定義
        spec.addSubcommand("process", CommandSpec.create()
            .addOption(OptionSpec.builder("--format")
                .description("Output format")
                .type(String.class)
                .defaultValue("json")
                .build()));

        CommandLine cmd = new CommandLine(spec);
        
        // カスタムExecutionStrategy
        cmd.setExecutionStrategy(new CommandLine.IExecutionStrategy() {
            public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException {
                System.out.println("Custom execution strategy");
                
                // パースされた値を取得
                boolean verbose = parseResult.matchedOptionValue("-v", false);
                File file = parseResult.matchedOptionValue("-f", null);
                
                if (verbose) {
                    System.out.println("Processing file: " + file);
                }
                
                return 0;
            }
        });

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

このように、picocliは基本的な使用から高度な機能まで、Javaでの強力なCLIアプリケーション開発を支援する包括的なフレームワークです。特にGraalVMとの統合により、従来のJavaアプリケーションでは実現困難だった高速起動と低メモリ使用量を実現できる点が大きな特徴です。