java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Picocli命令行工具

Java Picocli 实战指南:用注解写出好用的命令行工具

作者:唐青枫

使用Java的Picocli库来创建命令行工具是一种高效且简洁的方法,Picocli是一个小巧的库,它允许你通过注解的方式来定义命令行接口,从而使得命令行工具的创建变得非常简单,下面,我将通过一个实战示例来详细说明如何使用Picocli编写一个好用的命令行工具

简介

Picocli 是 Java 生态里很常用的命令行工具开发库。

它主要解决这些问题:

解析命令行参数
处理短选项和长选项
支持位置参数
自动做类型转换
生成帮助文档
生成版本信息
支持子命令
支持命令补全
支持打包成可执行 Jar
支持 GraalVM Native Image

一句话概括:

Picocli 用注解描述命令、选项和参数,把 String[] args 解析成 Java 对象。

没有 Picocli 时,命令行参数通常要手动解析:

public static void main(String[] args) {
    for (int i = 0; i < args.length; i++) {
        if ("--file".equals(args[i])) {
            String file = args[++i];
            System.out.println(file);
        }
    }
}

这种写法很快会变乱:

Picocli 把这些通用能力封装好了。

Picocli 适合什么场景

常见场景如下:

场景示例
开发者工具代码生成、项目初始化、接口调试
运维工具发布、备份、清理、巡检
数据处理工具CSV 转换、日志分析、批量导入
管理脚本用户管理、配置检查、任务触发
Spring Boot CLI复用 Service 做命令行管理工具
本地小工具文件重命名、图片处理、目录扫描

它和一些常见 CLI 工具的定位类似:

语言常见 CLI 框架
JavaPicocli
GoCobra
Pythonargparse / Click / Typer
Node.jsCommander
.NETSystem.CommandLine

Maven 依赖

当前 picocli 最新稳定版本是 4.7.7

Maven:

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

Gradle Kotlin DSL:

dependencies {
    implementation("info.picocli:picocli:4.7.7")
}

如果要生成 GraalVM Native Image 配置、命令补全脚本等,可以再加 picocli-codegen

<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli-codegen</artifactId>
    <version>4.7.7</version>
    <scope>provided</scope>
</dependency>

核心注解

Picocli 最常用的注解不多。

注解作用
@Command定义一个命令
@Option定义选项,比如 -f--file
@Parameters定义位置参数,比如 copy a.txt b.txt 里的两个文件
@Mixin复用一组公共选项
@ParentCommand在子命令里拿到父命令对象
@Spec拿到当前命令的元数据

一个命令类通常实现 RunnableCallable<Integer>

Runnable:只执行逻辑,不关心返回值
Callable<Integer>:可以返回退出码,更适合正式 CLI

正式工具更推荐 Callable<Integer>

退出码约定:

0:成功
非 0:失败

第一个 Demo:hello 命令

先写一个最简单的命令。

目标:

java -jar hello.jar 张三

输出:

Hello, 张三

代码:

package com.example.picoclidemo;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import java.util.concurrent.Callable;
@Command(
        name = "hello",
        description = "打印问候语",
        mixinStandardHelpOptions = true,
        version = "hello 1.0.0"
)
public class HelloCommand implements Callable<Integer> {
    @Parameters(index = "0", description = "名称")
    private String name;
    @Override
    public Integer call() {
        System.out.println("Hello, " + name);
        return 0;
    }
    public static void main(String[] args) {
        int exitCode = new CommandLine(new HelloCommand()).execute(args);
        System.exit(exitCode);
    }
}

运行:

java com.example.picoclidemo.HelloCommand 张三

输出:

Hello, 张三

查看帮助:

java com.example.picoclidemo.HelloCommand --help

输出类似:

Usage: hello [-hV] <name>
打印问候语
      <name>      名称
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

mixinStandardHelpOptions = true 会自动添加:

-h, --help
-V, --version

这两个选项几乎每个正式 CLI 都应该有。

Option 和 Parameters 的区别

命令行参数大致分两类。

Option

带名字的参数叫选项。

比如:

tool --file app.log --limit 100 --verbose

对应 Picocli:

@Option(names = {"-f", "--file"}, description = "文件路径")
private File file;
@Option(names = {"-l", "--limit"}, defaultValue = "100", description = "最多处理多少行")
private int limit;
@Option(names = {"-v", "--verbose"}, description = "输出详细日志")
private boolean verbose;

Parameters

没有名字、靠位置识别的参数叫位置参数。

比如:

copy source.txt target.txt

对应 Picocli:

@Parameters(index = "0", description = "源文件")
private File source;
@Parameters(index = "1", description = "目标文件")
private File target;

简单判断:

有 - 或 -- 前缀:@Option
没有前缀,靠顺序:@Parameters

实战 Demo:文件统计工具

下面做一个稍微实用点的工具:统计文本文件。

支持功能:

统计行数
统计字符数
忽略空行
限制最多读取多少行
输出详细信息

命令示例:

textstat README.md --chars --ignore-blank --limit 1000 -v

命令类

package com.example.picoclidemo;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;
@Command(
        name = "textstat",
        description = "统计文本文件的行数和字符数",
        mixinStandardHelpOptions = true,
        version = "textstat 1.0.0"
)
public class TextStatCommand implements Callable<Integer> {
    @Parameters(index = "0", description = "要统计的文本文件")
    private Path file;
    @Option(names = {"-c", "--chars"}, description = "统计字符数")
    private boolean countChars;
    @Option(names = {"--ignore-blank"}, description = "忽略空行")
    private boolean ignoreBlank;
    @Option(names = {"-l", "--limit"}, defaultValue = "0", description = "最多读取多少行,0 表示不限制")
    private int limit;
    @Option(names = {"-v", "--verbose"}, description = "输出详细信息")
    private boolean verbose;
    @Override
    public Integer call() {
        if (!Files.exists(file)) {
            System.err.println("文件不存在: " + file);
            return 2;
        }
        if (!Files.isRegularFile(file)) {
            System.err.println("不是普通文件: " + file);
            return 2;
        }
        try {
            List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
            if (limit > 0 && lines.size() > limit) {
                lines = lines.subList(0, limit);
            }
            long lineCount = lines.stream()
                    .filter(line -> !ignoreBlank || !line.isBlank())
                    .count();
            System.out.println("lines=" + lineCount);
            if (countChars) {
                int chars = lines.stream()
                        .filter(line -> !ignoreBlank || !line.isBlank())
                        .mapToInt(String::length)
                        .sum();
                System.out.println("chars=" + chars);
            }
            if (verbose) {
                System.out.println("file=" + file.toAbsolutePath());
                System.out.println("limit=" + limit);
                System.out.println("ignoreBlank=" + ignoreBlank);
            }
            return 0;
        } catch (IOException e) {
            System.err.println("读取文件失败: " + e.getMessage());
            return 1;
        }
    }
    public static void main(String[] args) {
        int exitCode = new CommandLine(new TextStatCommand()).execute(args);
        System.exit(exitCode);
    }
}

运行:

java com.example.picoclidemo.TextStatCommand README.md

输出:

lines=128

统计字符数:

java com.example.picoclidemo.TextStatCommand README.md --chars

输出:

lines=128
chars=5042

忽略空行并输出详细信息:

java com.example.picoclidemo.TextStatCommand README.md --ignore-blank -v

输出:

lines=96
file=/Users/test/project/README.md
limit=0
ignoreBlank=true

缺少文件参数:

java com.example.picoclidemo.TextStatCommand

Picocli 会直接提示参数缺失,并打印用法。

常用 Option 写法

必填选项

@Option(names = {"-u", "--username"}, required = true, description = "用户名")
private String username;

没传会报错:

Missing required option: '--username=<username>'

默认值

@Option(names = {"-p", "--port"}, defaultValue = "8080", description = "端口,默认 ${DEFAULT-VALUE}")
private int port;

帮助信息里可以用:

${DEFAULT-VALUE}

显示默认值。

布尔开关

@Option(names = {"-v", "--verbose"}, description = "详细输出")
private boolean verbose;

只要命令里出现 -v,值就是 true

tool -v
tool --verbose

多值参数

@Option(names = {"-t", "--tag"}, description = "标签,可重复")
private List<String> tags;

运行:

tool -t java -t cli -t demo

结果:

[java, cli, demo]

也可以用 split

@Option(names = "--tags", split = ",", description = "逗号分隔的标签")
private List<String> tags;

运行:

tool --tags java,cli,demo

可选值

有些选项传不传值都可以。

比如:

tool --color
tool --color=always
tool --color=never

写法:

@Option(
        names = "--color",
        arity = "0..1",
        fallbackValue = "always",
        defaultValue = "auto",
        description = "颜色模式: auto, always, never"
)
private String color;

含义:

不传 --color:auto
只传 --color:always
传 --color=never:never

枚举参数

enum OutputFormat {
    TEXT, JSON, CSV
}
@Option(names = {"-f", "--format"}, defaultValue = "TEXT", description = "输出格式: ${COMPLETION-CANDIDATES}")
private OutputFormat format;

COMPLETION-CANDIDATES 会显示候选值。

运行:

tool --format JSON

位置参数

单个位置参数:

@Parameters(index = "0", description = "输入文件")
private Path input;

多个位置参数:

@Parameters(index = "0", description = "源文件")
private Path source;
@Parameters(index = "1", description = "目标文件")
private Path target;

剩余参数:

@Parameters(index = "0..*", description = "文件列表")
private List<Path> files;

运行:

tool a.txt b.txt c.txt

类型转换

Picocli 内置了很多常见类型转换。

常见类型:

Java 类型命令行传值
Stringhello
int / Integer8080
long / Long10000
booleantrue / false,或布尔开关
File./a.txt
Path./a.txt
URLhttps://example.com
URIfile:///tmp/a.txt
DurationPT10S
LocalDate2026-01-01
EnumJSON

如果内置转换不够,可以写自定义转换器。

自定义 LocalDate 转换器

假设日期想支持:

2026-07-18

转换器:

package com.example.picoclidemo;
import picocli.CommandLine.ITypeConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LocalDateConverter implements ITypeConverter<LocalDate> {
    @Override
    public LocalDate convert(String value) {
        return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

使用:

@Option(names = "--date", converter = LocalDateConverter.class, description = "日期,格式 yyyy-MM-dd")
private LocalDate date;

运行:

tool --date 2026-07-18

子命令 Demo:filecli

复杂 CLI 一般不是一个命令解决所有问题,而是类似:

git add
git commit
kubectl get pods
docker image ls

Picocli 支持子命令。

下面做一个 filecli

filecli stat README.md
filecli copy a.txt b.txt --force
filecli delete temp.log --dry-run

主命令

package com.example.picoclidemo.filecli;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(
        name = "filecli",
        description = "文件处理命令行工具",
        mixinStandardHelpOptions = true,
        version = "filecli 1.0.0",
        subcommands = {
                StatCommand.class,
                CopyCommand.class,
                DeleteCommand.class
        }
)
public class FileCliCommand implements Runnable {
    @Override
    public void run() {
        CommandLine.usage(this, System.out);
    }
    public static void main(String[] args) {
        int exitCode = new CommandLine(new FileCliCommand()).execute(args);
        System.exit(exitCode);
    }
}

stat 子命令

package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
@Command(name = "stat", description = "查看文件基本信息")
public class StatCommand implements Callable<Integer> {
    @Parameters(index = "0", description = "文件路径")
    private Path file;
    @Override
    public Integer call() throws Exception {
        if (!Files.exists(file)) {
            System.err.println("文件不存在: " + file);
            return 2;
        }
        System.out.println("path=" + file.toAbsolutePath());
        System.out.println("size=" + Files.size(file));
        System.out.println("directory=" + Files.isDirectory(file));
        System.out.println("regularFile=" + Files.isRegularFile(file));
        return 0;
    }
}

copy 子命令

package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.Callable;
@Command(name = "copy", description = "复制文件")
public class CopyCommand implements Callable<Integer> {
    @Parameters(index = "0", description = "源文件")
    private Path source;
    @Parameters(index = "1", description = "目标文件")
    private Path target;
    @Option(names = {"-f", "--force"}, description = "覆盖已存在文件")
    private boolean force;
    @Override
    public Integer call() throws Exception {
        if (!Files.exists(source)) {
            System.err.println("源文件不存在: " + source);
            return 2;
        }
        if (Files.exists(target) && !force) {
            System.err.println("目标文件已存在,使用 --force 覆盖: " + target);
            return 3;
        }
        if (force) {
            Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
        } else {
            Files.copy(source, target);
        }
        System.out.println("复制完成: " + source + " -> " + target);
        return 0;
    }
}

delete 子命令

package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
@Command(name = "delete", description = "删除文件")
public class DeleteCommand implements Callable<Integer> {
    @Parameters(index = "0", description = "文件路径")
    private Path file;
    @Option(names = "--dry-run", description = "只打印将要执行的操作,不真正删除")
    private boolean dryRun;
    @Override
    public Integer call() throws Exception {
        if (!Files.exists(file)) {
            System.err.println("文件不存在: " + file);
            return 2;
        }
        if (dryRun) {
            System.out.println("[dry-run] 将删除: " + file.toAbsolutePath());
            return 0;
        }
        Files.delete(file);
        System.out.println("删除完成: " + file.toAbsolutePath());
        return 0;
    }
}

运行:

java com.example.picoclidemo.filecli.FileCliCommand stat README.md
java com.example.picoclidemo.filecli.FileCliCommand copy a.txt b.txt --force
java com.example.picoclidemo.filecli.FileCliCommand delete temp.log --dry-run

查看子命令帮助:

java com.example.picoclidemo.filecli.FileCliCommand copy --help

输出类似:

Usage: filecli copy [-f] <source> <target>
复制文件
      <source>   源文件
      <target>   目标文件
  -f, --force    覆盖已存在文件

复用公共选项:Mixin

很多子命令都会有公共参数,比如:

--verbose
--profile
--config

可以用 @Mixin 复用。

公共选项:

package com.example.picoclidemo.filecli;
import picocli.CommandLine.Option;
import java.nio.file.Path;
public class CommonOptions {
    @Option(names = {"-v", "--verbose"}, description = "输出详细日志")
    boolean verbose;
    @Option(names = "--config", description = "配置文件路径")
    Path config;
}

子命令使用:

@Mixin
private CommonOptions commonOptions;

完整示例:

@Command(name = "stat", description = "查看文件基本信息")
public class StatCommand implements Callable<Integer> {
    @Mixin
    private CommonOptions commonOptions;
    @Parameters(index = "0", description = "文件路径")
    private Path file;
    @Override
    public Integer call() throws Exception {
        if (commonOptions.verbose) {
            System.out.println("config=" + commonOptions.config);
        }
        System.out.println("path=" + file.toAbsolutePath());
        return 0;
    }
}

运行:

filecli stat README.md --verbose --config ./app.properties

交互式输入

密码、Token 这类敏感参数不适合直接写在命令里。

可以使用交互式输入:

@Option(names = "--username", required = true, description = "用户名")
private String username;
@Option(
        names = "--password",
        interactive = true,
        arity = "0..1",
        description = "密码,不在命令行中明文显示"
)
private char[] password;

运行:

tool --username admin --password

控制台会提示输入密码。

相比:

tool --password 123456

交互式输入更适合敏感信息。

原因是命令行参数可能被历史记录、进程列表、审计日志记录下来。

参数校验

Picocli 本身会做一些基本校验:

比如:

@Option(names = "--port", required = true)
private int port;

传入:

tool --port abc

会提示无法转换成整数。

更复杂的业务校验可以放到 call() 里。

示例:

@Option(names = "--port", defaultValue = "8080", description = "端口")
private int port;
@Override
public Integer call() {
    if (port < 1 || port > 65535) {
        System.err.println("端口范围必须是 1 到 65535");
        return 2;
    }
    return 0;
}

也可以使用自定义转换器提前拦截非法值。

错误处理和退出码

CommandLine.execute(args) 会返回退出码。

常见做法:

public static void main(String[] args) {
    int exitCode = new CommandLine(new AppCommand()).execute(args);
    System.exit(exitCode);
}

常见退出码可以这样约定:

退出码含义
0成功
1普通执行失败
2参数或输入文件问题
3目标已存在、状态冲突

Picocli 内置常量:

CommandLine.ExitCode.OK
CommandLine.ExitCode.USAGE
CommandLine.ExitCode.SOFTWARE

示例:

return CommandLine.ExitCode.OK;

如果需要统一处理异常,可以配置异常处理器:

CommandLine commandLine = new CommandLine(new FileCliCommand());
commandLine.setExecutionExceptionHandler((ex, cmd, parseResult) -> {
    cmd.getErr().println("执行失败: " + ex.getMessage());
    return 1;
});
int exitCode = commandLine.execute(args);
System.exit(exitCode);

帮助文档写好一点

命令行工具的帮助信息很重要。

推荐给 @Command 写清楚这些字段:

@Command(
        name = "filecli",
        description = "文件处理命令行工具",
        synopsisHeading = "%n用法:%n  ",
        descriptionHeading = "%n说明:%n",
        optionListHeading = "%n选项:%n",
        parameterListHeading = "%n参数:%n",
        commandListHeading = "%n子命令:%n",
        mixinStandardHelpOptions = true,
        version = "filecli 1.0.0"
)

选项描述里可以使用变量:

@Option(
        names = "--format",
        defaultValue = "TEXT",
        description = "输出格式: ${COMPLETION-CANDIDATES},默认 ${DEFAULT-VALUE}"
)
private OutputFormat format;

常见变量:

变量说明
${DEFAULT-VALUE}默认值
${COMPLETION-CANDIDATES}候选值
${FALLBACK-VALUE}可选值参数的 fallback 值

命令补全

Picocli 可以生成命令补全脚本。

补全能力包括:

添加 codegen 依赖后,可以生成脚本。

常见方式是在主命令里加入 AutoComplete.GenerateCompletion 子命令:

@Command(
        name = "filecli",
        mixinStandardHelpOptions = true,
        subcommands = {
                StatCommand.class,
                CopyCommand.class,
                DeleteCommand.class,
                picocli.AutoComplete.GenerateCompletion.class
        }
)
public class FileCliCommand implements Runnable {
    @Override
    public void run() {
        CommandLine.usage(this, System.out);
    }
}

生成脚本:

java -cp "target/filecli.jar" com.example.picoclidemo.filecli.FileCliCommand generate-completion

也可以用 picocli.AutoComplete 工具类按命令类生成。

不同 Shell 的安装方式不同,常见是把生成脚本放到 Bash、Zsh 或 Fish 的补全目录里。

打包成可执行 Jar

命令行工具通常希望这样运行:

java -jar filecli.jar stat README.md

可以用 Maven Shade Plugin 打包 fat jar。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>com.example.picoclidemo.filecli.FileCliCommand</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

打包:

mvn clean package

运行:

java -jar target/filecli-1.0.0.jar stat README.md

为了像普通命令一样运行,可以加一个脚本。

filecli

#!/usr/bin/env sh
java -jar /opt/filecli/filecli-1.0.0.jar "$@"

授权:

chmod +x filecli

运行:

./filecli stat README.md

GraalVM Native Image

CLI 工具很适合打成 Native Image。

好处:

Picocli 对 GraalVM Native Image 支持比较好。

Maven Native Build Tools 示例:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>1.1.2</version>
                    <extensions>true</extensions>
                    <configuration>
                        <mainClass>com.example.picoclidemo.filecli.FileCliCommand</mainClass>
                        <imageName>filecli</imageName>
                    </configuration>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

构建:

mvn -Pnative package

运行:

./target/filecli stat README.md

Native Image 构建需要安装 GraalVM 和本地 C 编译工具链。

如果项目引入了反射、动态代理、资源文件,可能还需要额外配置。纯 Picocli 小工具通常比较顺。

Spring Boot 集成

如果 CLI 需要复用 Spring Bean,比如:

可以使用 picocli-spring-boot-starter

依赖:

<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli-spring-boot-starter</artifactId>
    <version>4.7.7</version>
</dependency>

命令类:

package com.example.bootcli;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;
@Component
@Command(name = "user", description = "用户管理命令", mixinStandardHelpOptions = true)
public class UserCommand implements Callable<Integer> {
    private final UserService userService;
    @Option(names = "--list", description = "列出用户")
    private boolean list;
    public UserCommand(UserService userService) {
        this.userService = userService;
    }
    @Override
    public Integer call() {
        if (list) {
            userService.findAll().forEach(System.out::println);
            return 0;
        }
        System.out.println("没有指定操作");
        return 2;
    }
}

启动类:

package com.example.bootcli;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;
@SpringBootApplication
public class BootCliApplication implements CommandLineRunner {
    private final UserCommand userCommand;
    private final IFactory factory;
    public BootCliApplication(UserCommand userCommand, IFactory factory) {
        this.userCommand = userCommand;
        this.factory = factory;
    }
    public static void main(String[] args) {
        SpringApplication.run(BootCliApplication.class, args);
    }
    @Override
    public void run(String... args) {
        int exitCode = new CommandLine(userCommand, factory).execute(args);
        System.exit(exitCode);
    }
}

运行:

java -jar boot-cli.jar --list

这种方式的关键点是:

命令类交给 Spring 管理
CommandLine 使用 Spring 提供的 IFactory 创建对象
子命令、转换器等也可以走 Spring 注入

如果只是一个很小的 CLI,不一定需要 Spring Boot。Spring Boot 启动成本更高,适合需要复用完整业务容器的场景。

单元测试

CLI 工具也应该测试。

Picocli 测试很方便,可以直接构造命令对象,捕获输出。

示例命令:

@Command(name = "hello")
class HelloCommand implements Callable<Integer> {
    @Parameters(index = "0")
    String name;
    PrintWriter out = new PrintWriter(System.out, true);
    @Override
    public Integer call() {
        out.println("Hello, " + name);
        return 0;
    }
}

测试:

package com.example.picoclidemo;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HelloCommandTest {
    @Test
    void shouldPrintHello() {
        HelloCommand command = new HelloCommand();
        StringWriter output = new StringWriter();
        command.out = new PrintWriter(output);
        int exitCode = new CommandLine(command).execute("张三");
        assertEquals(0, exitCode);
        assertTrue(output.toString().contains("Hello, 张三"));
    }
}

也可以直接测试错误参数:

@Test
void shouldReturnUsageExitCodeWhenMissingName() {
    int exitCode = new CommandLine(new HelloCommand()).execute();
    assertEquals(CommandLine.ExitCode.USAGE, exitCode);
}

和 Apache Commons CLI 的区别

Java 里还有一个老牌库叫 Apache Commons CLI

两者大概区别:

对比项PicocliApache Commons CLI
开发方式注解驱动,也支持编程式 API编程式 API
帮助文档自动生成能力强相对基础
子命令支持较好需要自行组织
类型转换内置很多类型通常手动取字符串再转换
GraalVM支持较好不是主要卖点
适合场景完整 CLI 工具简单参数解析

新写命令行工具,Picocli 更省事。

如果只是解析几个参数,Commons CLI 也够用。

常见坑

main 方法里没有 System.exit

错误写法:

new CommandLine(new AppCommand()).execute(args);

脚本调用时无法正确拿到退出码。

推荐:

int exitCode = new CommandLine(new AppCommand()).execute(args);
System.exit(exitCode);

还在使用 CommandLine.run 或 call

旧示例里经常看到:

CommandLine.run(new AppCommand(), args);
CommandLine.call(new AppCommand(), args);

这些便捷方法在 Picocli 4.x 里已经不推荐。

推荐:

new CommandLine(new AppCommand()).execute(args);

布尔参数写成 Boolean 但没有默认值

@Option(names = "--verbose")
private Boolean verbose;

如果没传,值是 null

大部分布尔开关用基本类型更简单:

@Option(names = "--verbose")
private boolean verbose;

位置参数顺序不清楚

@Parameters
private Path source;
@Parameters
private Path target;

多个位置参数建议明确 index

@Parameters(index = "0")
private Path source;
@Parameters(index = "1")
private Path target;

选项名太随意

不建议写一堆只有短选项的参数:

-a -b -c -x

正式工具最好短选项和长选项都提供:

@Option(names = {"-f", "--file"})
@Option(names = {"-o", "--output"})
@Option(names = {"-v", "--verbose"})

长选项更适合脚本,可读性更好。

密码直接从参数传入

不推荐:

tool --password 123456

更推荐:

tool --password

配合:

@Option(names = "--password", interactive = true, arity = "0..1")
private char[] password;

总结

Picocli 可以按这条线理解:

@Command 定义命令
@Option 定义具名选项
@Parameters 定义位置参数
CommandLine.execute 负责解析和执行
Callable<Integer> 返回退出码
subcommands 组织复杂工具
Mixin 复用公共参数

日常开发可以按这个顺序落地:

Picocli 的价值不是“把参数解析换成注解”这么简单。真正省事的地方在于帮助文档、错误提示、子命令、类型转换、退出码这些细节都已经有一套成熟约定。命令行工具越往后做,越能感受到这些约定带来的稳定性。

到此这篇关于Java Picocli 实战指南:用注解写出好用的命令行工具的文章就介绍到这了,更多相关Java Picocli命令行工具内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文