JCommander

A popular annotation-based Java CLI library. Easy to use and well-documented.

javaclicommand-lineannotationargument-parsing

Framework

JCommander

Overview

JCommander is an annotation-based library for parsing Java command-line arguments. Developed by Cédric Beust, the author of TestNG, it features a declarative and intuitive API design using annotations. By simply adding @Parameter annotations to Java fields, it automatically handles command-line argument parsing, type conversion, validation, and help generation.

Why JCommander is chosen:

  • Annotation-based: Declarative and intuitive API design
  • Rich features: Type conversion, validation, subcommands, help generation
  • Ease of use: Achieve high-functionality CLI with minimal code
  • Stability: Long-term maintenance by the TestNG author
  • Internationalization support: Multi-language help message support

Details

History and Evolution

JCommander development began in 2010 by Cédric Beust. Born from the need for command-line parsing in the TestNG project, it has grown as a natural and user-friendly library for Java developers through its annotation-based design philosophy. It continues to be actively maintained and supports new features in Java 8 and later.

Position in the Ecosystem

It occupies an important position in the Java CLI library ecosystem:

  • TestNG ecosystem: Used for command-line parsing within TestNG
  • Enterprise tools: Rich adoption record in corporate internal tools
  • Education: Learning material for annotation-based design in Java
  • Legacy support: Stable choice for pre-Java 8 projects

Latest Trends (2024-2025)

  • Java 17+ support: Guaranteed operation on latest Java versions
  • Security enhancements: Dependency vulnerability countermeasures and security updates
  • Maven Central: Continuous releases and maintenance
  • Compatibility maintenance: Feature additions while maintaining backward compatibility of existing APIs
  • Performance optimization: Faster argument parsing and reduced memory usage

Key Features

Core Annotations

  • @Parameter: Bind command-line arguments to Java fields
  • @Parameters: Define subcommands
  • @DynamicParameter: Dynamic parameters for key-value pairs
  • @ParametersDelegate: Parameter grouping and delegation

Advanced Features

  • Type conversion: Automatic conversion of String input to various Java types (Integer, Boolean, Enum, etc.)
  • Custom converters: Custom conversion via IStringConverter interface
  • Validation: Required parameters, arity, custom validation
  • Help generation: Automatic usage generation based on annotations
  • Internationalization: Multi-language support via ResourceBundle
  • File reading: Parameter file reading with @ syntax
  • Password input: Secure input prompts

Subcommand Support

  • Hierarchical structure: Git-style nested command structure
  • Command separation: Independent parameter definitions for each subcommand
  • Dynamic addition: Runtime subcommand addition
  • Help integration: Subcommand-specific help display

Pros and Cons

Pros

  • Declarative design: Intuitive API through annotations
  • Rich functionality: Includes type conversion, validation, help generation
  • Reduced code: Achieve high functionality with minimal code
  • Type safety: Compile-time type checking
  • Stability: Stable API over long periods
  • Lightweight: Minimal dependencies

Cons

  • Annotation dependency: Constraints of annotation-based approach
  • Runtime errors: Annotation configuration mistakes discovered at runtime
  • Customization limits: Limitations for complex customizations
  • Lack of modernity: Slow adoption of new Java features
  • Documentation: Infrequent updates to official documentation

Key Links

Usage Examples

Basic Usage

import com.beust.jcommander.*;

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

    @Parameter(names = {"-v", "--verbose"}, description = "Verbose output")
    private boolean verbose = false;

    @Parameter(names = "--help", help = true, description = "Show help")
    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("Hello, " + app.name + "!");
            if (app.verbose) {
                System.out.println("Verbose mode is enabled");
            }
        } catch (ParameterException e) {
            System.err.println("Error: " + e.getMessage());
            jc.usage();
        }
    }
}

Type Conversion and Validation Example

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

public class ServerConfig {
    @Parameter(names = "--port", description = "Server port")
    private Integer port = 8080;

    @Parameter(names = "--hosts", description = "List of host names")
    private List<String> hosts;

    @Parameter(names = "--config", description = "Configuration file path", 
              converter = FileConverter.class)
    private File configFile;

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

    @Parameter(names = "--log-level", description = "Log level")
    private LogLevel logLevel = LogLevel.INFO;

    @Parameter(names = "--threads", description = "Number of threads", 
              validateWith = PositiveInteger.class)
    private int threads = 4;

    // Custom converter
    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);
        }
    }

    // Validator
    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("Parameter " + name + " should be positive (found " + 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("Server configuration:");
        System.out.println("  Port: " + config.port);
        System.out.println("  Hosts: " + config.hosts);
        System.out.println("  Config file: " + config.configFile);
        System.out.println("  Start date: " + config.startDate);
        System.out.println("  Log level: " + config.logLevel);
        System.out.println("  Threads: " + config.threads);
    }
}

Subcommand Example

import com.beust.jcommander.*;

// Main class
public class GitTool {
    @Parameter(names = {"-v", "--verbose"}, description = "Verbose output")
    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("Verbose mode is enabled");
        }

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

// Subcommand: add
@Parameters(commandNames = "add", commandDescription = "Add files to the staging area")
class AddCommand {
    @Parameter(description = "Files to add")
    private List<String> files;

    @Parameter(names = {"-A", "--all"}, description = "Add all files")
    private boolean all = false;

    public void execute() {
        if (all) {
            System.out.println("Adding all files");
        } else if (files != null && !files.isEmpty()) {
            System.out.println("Adding files: " + files);
        } else {
            System.out.println("No files specified to add");
        }
    }
}

// Subcommand: commit
@Parameters(commandNames = "commit", commandDescription = "Commit changes")
class CommitCommand {
    @Parameter(names = {"-m", "--message"}, description = "Commit message", required = true)
    private String message;

    @Parameter(names = {"-a", "--all"}, description = "Commit all changes")
    private boolean all = false;

    public void execute() {
        System.out.println("Committing: " + message);
        if (all) {
            System.out.println("Committing all changes");
        }
    }
}

// Subcommand: push
@Parameters(commandNames = "push", commandDescription = "Push changes to remote")
class PushCommand {
    @Parameter(names = "--remote", description = "Remote name")
    private String remote = "origin";

    @Parameter(names = "--branch", description = "Branch name")
    private String branch = "main";

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

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

Dynamic Parameters and Delegation Example

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

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

    // Dynamic parameters (-Dkey=value format)
    @DynamicParameter(names = "-D", description = "System properties")
    private Map<String, String> systemProperties = new HashMap<>();

    // Parameter delegation
    @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("Error: " + e.getMessage());
            jc.usage();
        }
    }

    private void execute() {
        System.out.println("Build tool running...");
        
        // Display system properties
        if (!systemProperties.isEmpty()) {
            System.out.println("System properties:");
            systemProperties.forEach((key, value) -> 
                System.out.println("  " + key + " = " + value));
        }

        // Display logging configuration
        System.out.println("Log level: " + loggingOptions.getLogLevel());
        System.out.println("Log file: " + loggingOptions.getLogFile());

        // Display database configuration
        if (databaseOptions.getUrl() != null) {
            System.out.println("Database URL: " + databaseOptions.getUrl());
            System.out.println("Username: " + databaseOptions.getUsername());
        }
    }
}

// Logging options group
class LoggingOptions {
    @Parameter(names = "--log-level", description = "Log level")
    private String logLevel = "INFO";

    @Parameter(names = "--log-file", description = "Log file path")
    private String logFile;

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

// Database options group
class DatabaseOptions {
    @Parameter(names = "--db-url", description = "Database URL")
    private String url;

    @Parameter(names = "--db-user", description = "Database username")
    private String username;

    @Parameter(names = "--db-password", description = "Database password", password = true)
    private String password;

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

Internationalization and Custom Help Example

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();
        
        // Configure resource bundle
        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("Name: " + app.name);
            if (app.age > 0) {
                System.out.println("Age: " + app.age);
            }
        } catch (ParameterException e) {
            System.err.println("Error: " + e.getMessage());
            jc.usage();
        }
    }
}

Corresponding resource file messages.properties:

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

Japanese version messages_ja.properties:

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

Loading Parameters from File

import com.beust.jcommander.*;

public class FileParameterExample {
    @Parameter(names = "--config", description = "Configuration file path")
    private String configFile;

    @Parameter(names = "--output", description = "Output directory")
    private String outputDir = "build";

    @Parameter(names = "--verbose", description = "Verbose output")
    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("Config file: " + app.configFile);
        System.out.println("Output directory: " + app.outputDir);
        System.out.println("Verbose output: " + app.verbose);
    }
}

Parameter file build.args:

--output
dist
--verbose

Execution examples:

# Load parameters from file
java FileParameterExample @build.args --config app.properties

# Can be combined with regular command-line arguments
java FileParameterExample @build.args --output custom-output