1. 简介

在本教程中,我们将介绍 Picocli 这个库,它可以帮助我们快速构建 Java 命令行程序。

我们会从一个简单的 Hello World 命令开始,然后深入 Picocli 的关键特性,通过模拟实现部分 git 命令的功能来展示其强大之处。


2. Hello World 命令

我们先从一个简单的 Hello World 示例开始。

首先,确保在 pom.xml 中添加 Picocli 依赖:

<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>4.7.0</version>
</dependency>

接着,创建一个类并使用 @Command 注解来定义命令:

@Command(
  name = "hello",
  description = "输出 Hello World"
)
public class HelloWorldCommand implements Runnable {
    public static void main(String[] args) {
        CommandLine.run(new HelloWorldCommand(), args);
    }

    @Override
    public void run() {
        System.out.println("Hello World!");
    }
}

运行 main 方法后,控制台将输出:

Hello World!

如果你打包成 jar 文件,也可以通过如下命令运行:

java -cp "picocli.jar;your-command.jar" com.example.HelloWorldCommand

3. 实战案例:模拟 Git 命令

接下来,我们将用 Picocli 实现一个类似 git 的命令结构,包括子命令、选项和参数。

首先,创建主命令类 GitCommand

@Command
public class GitCommand implements Runnable {
    public static void main(String[] args) {
        CommandLine.run(new GitCommand(), args);
    }

    @Override
    public void run() {
        System.out.println("这是模拟的 git 命令");
    }
}

4. 添加子命令

Git 有多个子命令,如 addcommit 等。Picocli 提供了三种方式来注册子命令。

4.1 使用 @Command 注解在类上

@Command(
  name = "git",
  subcommands = {GitAddCommand.class, GitCommitCommand.class}
)
public class GitCommand implements Runnable {
    // ...
}

子命令类示例:

@Command(name = "add")
public class GitAddCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("添加文件到暂存区");
    }
}

@Command(name = "commit")
public class GitCommitCommand implements Runnable {
    @Override
    public void run() {
        System.out.println("提交暂存区内容");
    }
}

运行命令:

git add

输出:

添加文件到暂存区

4.2 使用 @Command 注解在方法上

你也可以在主类中定义方法并用 @Command 注解:

@Command(name = "add")
public void add() {
    System.out.println("添加文件到暂存区");
}

@Command(name = "commit")
public void commit() {
    System.out.println("提交暂存区内容");
}

这种方式更简洁,适合逻辑简单的命令。

4.3 编程方式添加子命令

适用于需要动态注册子命令的场景:

CommandLine commandLine = new CommandLine(new GitCommand());
commandLine.addSubcommand("add", new GitAddCommand());
commandLine.addSubcommand("commit", new GitCommitCommand());

commandLine.parseWithHandler(new CommandLine.RunLast(), args);

注意:此时不能使用 CommandLine.run(),需要手动调用 parseWithHandler()


5. 使用 @Option 注解管理选项

5.1 无参数选项

例如,添加 -A--all 选项来表示添加所有文件:

@Option(names = {"-A", "--all"})
private boolean all;

@Override
public void run() {
    if (all) {
        System.out.println("添加所有文件到暂存区");
    } else {
        System.out.println("添加部分文件到暂存区");
    }
}

5.2 有参数选项

比如 commit 命令的 -m 消息参数:

@Option(names = {"-m", "--message"})
private String message;

@Override
public void run() {
    if (message != null) {
        System.out.println("提交信息: " + message);
    }
}

5.3 多参数选项

支持多次使用 -m

@Option(names = {"-m", "--message"})
private String[] messages;

@Override
public void run() {
    for (String msg : messages) {
        System.out.println("提交信息: " + msg);
    }
}

也可以使用分隔符一次性输入:

@Option(names = {"-m", "--message"}, split = ",")
private String[] messages;

使用方式:

git commit -m "first msg,second msg"

5.4 必填选项

标记某个选项为必填:

@Option(names = {"-m", "--message"}, required = true)
private String[] messages;

如果未提供 -m,Picocli 会输出错误信息并显示帮助。


6. 使用 @Parameters 管理位置参数

6.1 捕获位置参数

比如 add file1 file2

@Parameters
private List<Path> files;

@Override
public void run() {
    files.forEach(file -> System.out.println("添加文件: " + file));
}

6.2 捕获指定位置参数

使用 index 参数选择特定位置:

@Parameters(index = "2..*")
private List<Path> files;

这将捕获第 3 个及之后的所有参数。


7. 类型转换机制

Picocli 内置支持多种类型转换,例如 PathIntegerEnum 等。

自定义类型转换

比如我们定义一个配置项枚举:

public enum ConfigElement {
    USERNAME("user.name"), EMAIL("user.email");

    private final String value;

    ConfigElement(String value) {
        this.value = value;
    }

    public String value() {
        return value;
    }

    public static ConfigElement from(String value) {
        return Arrays.stream(values())
            .filter(e -> e.value.equals(value))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("无效配置项: " + value));
    }
}

在命令中使用:

@Parameters(index = "0")
private ConfigElement element;

@Parameters(index = "1")
private String value;

@Override
public void run() {
    System.out.println("设置 " + element.value() + " 为 " + value);
}

注册转换器:

CommandLine cmd = new CommandLine(new GitCommand());
cmd.registerConverter(ConfigElement.class, ConfigElement::from);
cmd.parseWithHandler(new RunLast(), args);

当传入非法参数时,会提示错误并显示帮助信息。


8. 与 Spring Boot 集成

如果你想在 Spring Boot 项目中使用 Picocli,可以这样做:

定义 Spring Boot 主类:

@SpringBootApplication
public class Application implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    private final GitCommand gitCommand;
    private final GitAddCommand addCommand;
    private final GitCommitCommand commitCommand;
    private final GitConfigCommand configCommand;

    public Application(GitCommand gitCommand, GitAddCommand addCommand,
                      GitCommitCommand commitCommand, GitConfigCommand configCommand) {
        this.gitCommand = gitCommand;
        this.addCommand = addCommand;
        this.commitCommand = commitCommand;
        this.configCommand = configCommand;
    }

    @Override
    public void run(String... args) {
        CommandLine cmd = new CommandLine(gitCommand);
        cmd.addSubcommand("add", addCommand);
        cmd.addSubcommand("commit", commitCommand);
        cmd.addSubcommand("config", configCommand);

        cmd.parseWithHandler(new CommandLine.RunLast(), args);
    }
}

同时,确保你的命令类标注 @Component,以便 Spring 扫描到它们。

这样,你就可以在命令类中注入 Spring Bean,实现依赖注入。


9. 总结 ✅

本文介绍了 Picocli 的核心功能:

  • 创建基本命令
  • 添加子命令(类、方法、编程方式)
  • 使用 @Option 定义各种选项
  • 使用 @Parameters 捕获位置参数
  • 自定义类型转换
  • 与 Spring Boot 集成

Picocli 是一个非常强大的命令行解析库,支持自动帮助生成、嵌套命令、类型转换、自定义解析逻辑等。它的文档非常完整,建议进一步阅读:Picocli 官方文档

如果你正在开发需要复杂 CLI 的 Java 项目,Picocli 绝对是一个值得考虑的选项。


原始标题:Create a Java Command Line Program with Picocli | Baeldung