JCommander

人気のあるアノテーションベースのJava CLIライブラリ。使いやすく、よくドキュメント化されています。

javaclicommand-lineannotationargument-parsing

フレームワーク

JCommander

概要

JCommanderは、Javaのコマンドライン引数を解析するためのアノテーションベースのライブラリです。TestNGの作者であるCédric Beust氏によって開発され、アノテーションを使った宣言的で直感的なAPI設計が特徴です。Javaフィールドに@Parameterアノテーションを付与するだけで、コマンドライン引数の解析、型変換、バリデーション、ヘルプ生成を自動的に行うことができます。

なぜJCommanderが選ばれるのか:

  • アノテーションベース: 宣言的で直感的なAPI設計
  • 豊富な機能: 型変換、バリデーション、サブコマンド、ヘルプ生成
  • 使いやすさ: 最小限のコードで高機能なCLIを実現
  • 安定性: TestNG作者による長期間のメンテナンス
  • 国際化サポート: 多言語でのヘルプメッセージ対応

詳細

歴史と発展

JCommanderは2010年にCédric Beust氏によって開発が開始されました。TestNGプロジェクトでのコマンドライン解析のニーズから生まれ、アノテーションベースの設計思想により、Java開発者にとって自然で使いやすいライブラリとして成長しました。現在でも積極的にメンテナンスされており、Java8以降の新機能にも対応しています。

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

Java CLIライブラリのエコシステムにおいて重要な位置を占めています:

  • TestNGエコシステム: TestNGの内部でコマンドライン解析に使用
  • エンタープライズツール: 企業内ツールでの採用実績が豊富
  • 教育分野: Javaにおけるアノテーションベース設計の学習教材
  • Legacy支援: Java8以前のプロジェクトでの安定した選択肢

最新動向(2024-2025年)

  • Java17+対応: 最新のJavaバージョンでの動作保証
  • セキュリティ強化: 依存関係の脆弱性対策とセキュリティアップデート
  • Maven Central: 継続的なリリースとメンテナンス
  • 互換性維持: 既存APIの下位互換性を保ちながらの機能追加
  • パフォーマンス最適化: 引数解析の高速化とメモリ使用量の削減

主な特徴

コアアノテーション

  • @Parameter: コマンドライン引数をJavaフィールドにバインド
  • @Parameters: サブコマンドの定義
  • @DynamicParameter: キー・バリューペアの動的パラメータ
  • @ParametersDelegate: パラメータのグループ化と委譲

高度な機能

  • 型変換: String入力を各種Java型(Integer、Boolean、Enum等)に自動変換
  • カスタムコンバーター: IStringConverterインターフェースによる独自変換
  • バリデーション: 必須パラメータ、arity、カスタムバリデーション
  • ヘルプ生成: アノテーションベースの自動usage生成
  • 国際化: ResourceBundleによる多言語サポート
  • ファイル読み込み: @構文によるパラメータファイルの読み込み
  • パスワード入力: セキュアな入力プロンプト

サブコマンドサポート

  • 階層構造: git風のネストしたコマンド構造
  • コマンド分離: 各サブコマンドの独立したパラメータ定義
  • 動的追加: 実行時のサブコマンド追加
  • ヘルプ統合: サブコマンド別のヘルプ表示

メリット・デメリット

メリット

  • 宣言的設計: アノテーションによる直感的なAPI
  • 豊富な機能: 型変換、バリデーション、ヘルプ生成を内包
  • コード量の削減: 最小限のコードで高機能を実現
  • 型安全性: コンパイル時の型チェック
  • 安定性: 長期間にわたる安定したAPI
  • 軽量: 最小限の依存関係

デメリット

  • アノテーション依存: アノテーションベースの制約
  • 実行時エラー: アノテーション設定ミスが実行時に発覚
  • カスタマイズの制限: 複雑なカスタマイズには限界
  • モダンさの不足: 新しいJava機能への対応の遅れ
  • ドキュメント: 公式ドキュメントの更新頻度

主要リンク

書き方の例

基本的な使用例

import com.beust.jcommander.*;

public class HelloWorld {
    @Parameter(names = "--name", description = "ユーザー名", required = true)
    private String name;

    @Parameter(names = {"-v", "--verbose"}, description = "詳細出力")
    private boolean verbose = false;

    @Parameter(names = "--help", help = true, description = "ヘルプを表示")
    private boolean help;

    public static void main(String[] args) {
        HelloWorld app = new HelloWorld();
        JCommander jc = JCommander.newBuilder()
            .addObject(app)
            .build();

        try {
            jc.parse(args);
            
            if (app.help) {
                jc.usage();
                return;
            }

            System.out.println("こんにちは、" + app.name + "さん!");
            if (app.verbose) {
                System.out.println("詳細モードが有効です");
            }
        } catch (ParameterException e) {
            System.err.println("エラー: " + e.getMessage());
            jc.usage();
        }
    }
}

型変換とバリデーションの例

import com.beust.jcommander.*;
import java.io.File;
import java.time.LocalDate;
import java.util.List;

public class ServerConfig {
    @Parameter(names = "--port", description = "サーバーポート")
    private Integer port = 8080;

    @Parameter(names = "--hosts", description = "ホスト名のリスト")
    private List<String> hosts;

    @Parameter(names = "--config", description = "設定ファイルパス", 
              converter = FileConverter.class)
    private File configFile;

    @Parameter(names = "--start-date", description = "開始日(YYYY-MM-DD)",
              converter = LocalDateConverter.class)
    private LocalDate startDate;

    @Parameter(names = "--log-level", description = "ログレベル")
    private LogLevel logLevel = LogLevel.INFO;

    @Parameter(names = "--threads", description = "スレッド数", 
              validateWith = PositiveInteger.class)
    private int threads = 4;

    // カスタムコンバーター
    public static class FileConverter implements IStringConverter<File> {
        @Override
        public File convert(String value) {
            return new File(value);
        }
    }

    public static class LocalDateConverter implements IStringConverter<LocalDate> {
        @Override
        public LocalDate convert(String value) {
            return LocalDate.parse(value);
        }
    }

    // バリデーター
    public static class PositiveInteger implements IParameterValidator {
        @Override
        public void validate(String name, String value) throws ParameterException {
            int n = Integer.parseInt(value);
            if (n <= 0) {
                throw new ParameterException("パラメータ " + name + " は正の整数である必要があります (入力値: " + value + ")");
            }
        }
    }

    public enum LogLevel {
        DEBUG, INFO, WARN, ERROR
    }

    public static void main(String[] args) {
        ServerConfig config = new ServerConfig();
        JCommander.newBuilder()
            .addObject(config)
            .build()
            .parse(args);

        System.out.println("サーバー設定:");
        System.out.println("  ポート: " + config.port);
        System.out.println("  ホスト: " + config.hosts);
        System.out.println("  設定ファイル: " + config.configFile);
        System.out.println("  開始日: " + config.startDate);
        System.out.println("  ログレベル: " + config.logLevel);
        System.out.println("  スレッド数: " + config.threads);
    }
}

サブコマンドの例

import com.beust.jcommander.*;

// メインクラス
public class GitTool {
    @Parameter(names = {"-v", "--verbose"}, description = "詳細出力")
    private boolean verbose = false;

    public static void main(String[] args) {
        GitTool main = new GitTool();
        AddCommand addCmd = new AddCommand();
        CommitCommand commitCmd = new CommitCommand();
        PushCommand pushCmd = new PushCommand();

        JCommander jc = JCommander.newBuilder()
            .addObject(main)
            .addCommand("add", addCmd)
            .addCommand("commit", commitCmd)
            .addCommand("push", pushCmd)
            .build();

        jc.parse(args);

        String parsedCommand = jc.getParsedCommand();
        if (parsedCommand == null) {
            jc.usage();
            return;
        }

        if (main.verbose) {
            System.out.println("詳細モードが有効です");
        }

        switch (parsedCommand) {
            case "add":
                addCmd.execute();
                break;
            case "commit":
                commitCmd.execute();
                break;
            case "push":
                pushCmd.execute();
                break;
        }
    }
}

// サブコマンド: add
@Parameters(commandNames = "add", commandDescription = "ファイルをステージングエリアに追加")
class AddCommand {
    @Parameter(description = "追加するファイル")
    private List<String> files;

    @Parameter(names = {"-A", "--all"}, description = "すべてのファイルを追加")
    private boolean all = false;

    public void execute() {
        if (all) {
            System.out.println("すべてのファイルを追加します");
        } else if (files != null && !files.isEmpty()) {
            System.out.println("ファイルを追加: " + files);
        } else {
            System.out.println("追加するファイルが指定されていません");
        }
    }
}

// サブコマンド: commit
@Parameters(commandNames = "commit", commandDescription = "変更をコミット")
class CommitCommand {
    @Parameter(names = {"-m", "--message"}, description = "コミットメッセージ", required = true)
    private String message;

    @Parameter(names = {"-a", "--all"}, description = "全ての変更をコミット")
    private boolean all = false;

    public void execute() {
        System.out.println("コミット: " + message);
        if (all) {
            System.out.println("全ての変更をコミットします");
        }
    }
}

// サブコマンド: push
@Parameters(commandNames = "push", commandDescription = "変更をリモートにプッシュ")
class PushCommand {
    @Parameter(names = "--remote", description = "リモート名")
    private String remote = "origin";

    @Parameter(names = "--branch", description = "ブランチ名")
    private String branch = "main";

    @Parameter(names = {"-f", "--force"}, description = "強制プッシュ")
    private boolean force = false;

    public void execute() {
        System.out.println("プッシュ先: " + remote + "/" + branch);
        if (force) {
            System.out.println("強制プッシュモード");
        }
    }
}

動的パラメータと委譲の例

import com.beust.jcommander.*;
import java.util.Map;
import java.util.HashMap;

public class BuildTool {
    @Parameter(names = "--help", help = true)
    private boolean help;

    // 動的パラメータ(-Dkey=value形式)
    @DynamicParameter(names = "-D", description = "システムプロパティ")
    private Map<String, String> systemProperties = new HashMap<>();

    // パラメータ委譲
    @ParametersDelegate
    private LoggingOptions loggingOptions = new LoggingOptions();

    @ParametersDelegate
    private DatabaseOptions databaseOptions = new DatabaseOptions();

    public static void main(String[] args) {
        BuildTool tool = new BuildTool();
        JCommander jc = JCommander.newBuilder()
            .addObject(tool)
            .build();

        try {
            jc.parse(args);
            
            if (tool.help) {
                jc.usage();
                return;
            }

            tool.execute();
        } catch (ParameterException e) {
            System.err.println("エラー: " + e.getMessage());
            jc.usage();
        }
    }

    private void execute() {
        System.out.println("ビルドツール実行中...");
        
        // システムプロパティの表示
        if (!systemProperties.isEmpty()) {
            System.out.println("システムプロパティ:");
            systemProperties.forEach((key, value) -> 
                System.out.println("  " + key + " = " + value));
        }

        // ロギング設定の表示
        System.out.println("ログレベル: " + loggingOptions.getLogLevel());
        System.out.println("ログファイル: " + loggingOptions.getLogFile());

        // データベース設定の表示
        if (databaseOptions.getUrl() != null) {
            System.out.println("データベースURL: " + databaseOptions.getUrl());
            System.out.println("ユーザー名: " + databaseOptions.getUsername());
        }
    }
}

// ロギングオプションのグループ
class LoggingOptions {
    @Parameter(names = "--log-level", description = "ログレベル")
    private String logLevel = "INFO";

    @Parameter(names = "--log-file", description = "ログファイルパス")
    private String logFile;

    public String getLogLevel() { return logLevel; }
    public String getLogFile() { return logFile; }
}

// データベースオプションのグループ
class DatabaseOptions {
    @Parameter(names = "--db-url", description = "データベースURL")
    private String url;

    @Parameter(names = "--db-user", description = "データベースユーザー名")
    private String username;

    @Parameter(names = "--db-password", description = "データベースパスワード", password = true)
    private String password;

    public String getUrl() { return url; }
    public String getUsername() { return username; }
    public String getPassword() { return password; }
}

国際化とカスタムヘルプの例

import com.beust.jcommander.*;
import java.util.ResourceBundle;

public class I18nExample {
    @Parameter(names = "--name", descriptionKey = "name.description", required = true)
    private String name;

    @Parameter(names = "--age", descriptionKey = "age.description")
    private int age = 0;

    @Parameter(names = "--help", help = true, descriptionKey = "help.description")
    private boolean help;

    public static void main(String[] args) {
        I18nExample app = new I18nExample();
        
        // リソースバンドルの設定
        ResourceBundle bundle = ResourceBundle.getBundle("messages");
        
        JCommander jc = JCommander.newBuilder()
            .addObject(app)
            .resourceBundle(bundle)
            .programName("i18n-example")
            .build();

        try {
            jc.parse(args);
            
            if (app.help) {
                jc.usage();
                return;
            }

            System.out.println("名前: " + app.name);
            if (app.age > 0) {
                System.out.println("年齢: " + app.age);
            }
        } catch (ParameterException e) {
            System.err.println("エラー: " + e.getMessage());
            jc.usage();
        }
    }
}

対応するリソースファイル messages.properties:

name.description=ユーザーの名前を指定します
age.description=ユーザーの年齢を指定します(オプション)
help.description=ヘルプメッセージを表示します

英語版 messages_en.properties:

name.description=Specify the user's name
age.description=Specify the user's age (optional)
help.description=Display help message

ファイルからのパラメータ読み込み

import com.beust.jcommander.*;

public class FileParameterExample {
    @Parameter(names = "--config", description = "設定ファイルパス")
    private String configFile;

    @Parameter(names = "--output", description = "出力ディレクトリ")
    private String outputDir = "build";

    @Parameter(names = "--verbose", description = "詳細出力")
    private boolean verbose = false;

    public static void main(String[] args) {
        FileParameterExample app = new FileParameterExample();
        JCommander jc = JCommander.newBuilder()
            .addObject(app)
            .build();

        jc.parse(args);

        System.out.println("設定ファイル: " + app.configFile);
        System.out.println("出力ディレクトリ: " + app.outputDir);
        System.out.println("詳細出力: " + app.verbose);
    }
}

パラメータファイル build.args:

--output
dist
--verbose

実行例:

# ファイルからパラメータを読み込み
java FileParameterExample @build.args --config app.properties

# 通常のコマンドライン引数と組み合わせ可能
java FileParameterExample @build.args --output custom-output