JCommander
A popular annotation-based Java CLI library. Easy to use and well-documented.
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