使用 Picocli 开发 Java 命令行,5 分钟上手

大家好,我是鱼皮,对不会前端的同学来说,开发 命令行工具 是一种不错的展示系统功能的方式。在 Java 中开发命令行工具也很简单,使用框架,几分钟就能学会啦~

Picocli 入门

Picocli 是 Java 中个人认为功能最完善、最简单易用的命令行开发框架,可以帮助大家快速开发命令行工具。

网上有关 Picocli 框架的教程非常少,最推荐的入门方式除了看鱼皮的教程外,就是阅读官方文档了。

官方文档:https://picocli.info/

推荐从官方提供的快速入门教程开始:https://picocli.info/quick-guide.html

一般我们学习新技术的步骤是:先跑通入门 Demo,再学习该技术的用法和特性。

入门 Demo

1)在 Maven 项目的 pom.xml 文件中引入 picocli 的依赖:

<!-- https://picocli.info -->
<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>4.7.5</version>
</dependency>

然后我们在 com.yupi 包下新建 cli.example 包,用于存放所有和 Picocli 入门有关的示例代码。

2)复制官方快速入门教程中的示例代码到 com.yupi.cli.example 包下,并略微修改 run 方法中的代码,打印参数的值。

完整代码如下:

package com.yupi.cli.example;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@Command(name = "ASCIIArt", version = "ASCIIArt 1.0", mixinStandardHelpOptions = true
public class ASCIIArt implements Runnable 

    @Option(names = { "-s""--font-size" }, description = "Font size"
    int fontSize = 19;

    @Parameters(paramLabel = "<word>", defaultValue = "Hello, picocli"
               description = "Words to be translated into ASCII art.")
    private String[] words = { "Hello,""picocli" }; 

    @Override
    public void run() {
        // 自己实现业务逻辑
        System.out.println("fontSize = " + fontSize);
        System.out.println("words = " + String.join(",", words));
    }

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

看不懂这段代码没关系,官方文档已经给了非常详细的解释:

帮大家翻译一下:

  1. 创建一个实现 RunnableCallable 接口的类,这就是一个命令。
  2. 使用 @Command 注解标记该类并为其命名, mixinStandardHelpOptions 属性设置为 true 可以给应用程序自动添加 --help--version 选项。
  3. 通过 @Option 注解将字段设置为命令行选项,可以给选项设置名称和描述。
  4. 通过 @Parameters 注解将字段设置为命令行参数,可以指定默认值、描述等信息。
  5. Picocli 会将命令行参数转换为强类型值,并自动注入到注解字段中。
  6. 在类的 runcall 方法中定义业务逻辑,当命令解析成功(用户敲了回车)后被调用。
  7. main 方法中,通过 CommandLine 对象的 execute 方法来处理用户输入的命令,剩下的就交给 Picocli 框架来解析命令并执行业务逻辑啦~
  8. CommandLine.execute 方法返回一个退出代码。可以调用 System.exit 并将该退出代码作为参数,从而向调用进程表示成功或失败。

3)让我们更改主程序的执行参数(args)来测试程序,能够成功看到输出结果,如下图:

通过这个入门 Demo,我们可以简单总结一个命令的开发流程:

  1. 创建命令
  2. 设置选项和参数
  3. 编写命令执行的业务逻辑
  4. 通过 CommandLine 对象接受输入并执行命令

在跑通了入门 Demo 后,我们来学习一些 Picocli 开发命令行的实用功能。

实用功能

1、帮助手册

通过给类添加的 @Command 注解参数 mixinStandardHelpOptions 设置为 true 来开启:

@Command(name = "ASCIIArt", mixinStandardHelpOptions = true

然后将主程序的输入参数设置为 --help 就能打印出命令的帮助手册信息了,如下图:

可以看到,Picocli 生成的帮助手册不仅规范、而且清晰完整。

2、命令解析

Picocli 最核心的能力就是命令解析,能够从一句完整的命令中解析选项和参数,并填充到对象的属性中。

Picocli 使用注解的方式实现命令解析,不需要自己编写代码,整个类看起来非常清晰。

最核心的 2 个注解其实在入门 Demo 中我们已经使用到了:

  • @Option 注解用于解析选项
  • @Parameters 注解用于解析参数

示例代码如下:

@Option(names = { "-s""--font-size" }, description = "Font size"
int fontSize = 19;

@Parameters(paramLabel = "<word>", defaultValue = "Hello, picocli"
           description = "Words to be translated into ASCII art.")
private String[] words = { "Hello,""picocli" }; 

可以给这些注解指定参数,比较常用的参数有:

1)@Option 注解的 names 参数:指定选项英文名称。

2)description 参数:指定描述信息,从而让生成的帮助手册和提示信息更清晰。

3)@Parameters 注解的 paramLabel 参数:参数标签,作用类似于描述信息。

4)@Parameters 注解的 defaultValue 参数:默认值,参考文档:https://picocli.info/#_default_values

5)required 参数:要求必填,参考文档:https://picocli.info/#_required_arguments

示例代码如下:

class RequiredOption {
    
    @Option(names = "-a", required = true)
    String author;
}

此外,命令解析天然支持 多值选项,只需要把对象属性的类型设置为数组类型即可,比如:

@Option(names = "-option")
int[] values;

具体可以参考官方文档:https://picocli.info/#_multiple_values

更多关于选项和参数注解的用法,也可以阅读官方文档学习:https://picocli.info/quick-guide.html#_options_and_parameters

3、交互式输入

所谓的交互式输入就是允许用户像跟程序聊天一样,在程序的指引下一个参数一个参数地输入。

如下图:

Picocli 为交互式输入提供了很好的支持,我梳理了大概 4 种交互式输入的模式。

1)基本能力

交互式输入的一个典型应用场景就是:用户要登录时,引导 ta 输入密码。

官方已经为我们提供了一段交互式输入的示例代码,鱼皮对它进行了简化,示例代码如下:

参考官方文档:https://picocli.info/#_interactive_password_options

package com.yupi.cli.example;

import picocli.CommandLine;
import picocli.CommandLine.Option;

import java.util.concurrent.Callable;

public class Login implements Callable<Integer{
    @Option(names = {"-u""--user"}, description = "User name")
    String user;

    @Option(names = {"-p""--password"}, description = "Passphrase", interactive = true)
    String password;

    public Integer call() throws Exception {
        System.out.println("password = " + password);
        return 0;
    }

    public static void main(String[] args) {
        new CommandLine(new Login()).execute("-u""user123""-p");
    }
}

让我们分析下上面的代码,主要包含 4 个部分:

1)首先命令类需要实现 Callable 接口

public class Login implements Callable<Integer{
 ...
}

2)将 @Option 注解的 interactive 参数设置为 true,表示该选项支持交互式输入

@Option(names = {"-p""--password"}, interactive = true)
String password;

3)在所有参数都输入完成后,会执行 call 方法,可以在该方法中编写具体的业务逻辑:

public Integer call() throws Exception {
    System.out.println("password = " + password);
    return 0;
}

4)在 Main 方法中执行命令并传入参数:

new CommandLine(new Login()).execute("-u""user123""-p");

执行上述代码,看到程序提示我们输入密码:

注意,如果以 jar 包方式运行上述程序,用户的输入默认是不会显示在控制台的(类似输入密码时的体验)。从 Picocli 4.6 版本开始,可以通过指定 @Option 注解的 echo 参数为 true 来显示用户的输入,并通过 prompt 参数指定引导用户输入的提示语。

2)多个选项交互式

Picocli 支持在一个命令中指定多个交互式输入的选项,会按照顺序提示用户并接收输入。

在上述代码中再增加一个 checkPassword 选项,同样开启交互式输入,代码如下:

public class Login implements Callable<Integer{
    @Option(names = {"-u""--user"}, description = "User name")
    String user;

    @Option(names = {"-p""--password"}, description = "Passphrase", interactive = true)
    String password;

    @Option(names = {"-cp""--checkPassword"}, description = "Check Password", interactive = true)
    String checkPassword;

    public Integer call() throws Exception {
        System.out.println("password = " + password);
        System.out.println("checkPassword = " + checkPassword);
        return 0;
    }

    public static void main(String[] args) {
        new CommandLine(new Login()).execute("-u""user123""-p");
    }
}

但运行上述代码我们会发现,怎么只提示我输入了密码,没提示我输入确认密码呢?

这是由于 Picocli 框架的规则,用户必须在命令中指定需要交互式输入的选项(比如 -p),才会引导用户输入。

所以我们需要修改上述代码中的 main 方法,给命令输入补充 -cp 参数:

public static void main(String[] args) {
    new CommandLine(new Login()).execute("-u""user123""-p""-cp");
}

再次执行,这下程序会依次提醒我们输入两个选项啦:

根据实际使用情况,又可以将交互式输入分为 2 种模式:

  • 可选交互式:用户可以直接在整行命令中输入选项,而不用给用户提示信息。
  • 强制交互式:用户必须获得提示并输入某个选项,不允许不填写。

下面分别讲解这两种模式。

3)可选交互式

默认情况下,是无法直接在命令中给交互式选项指定任何参数的,只能通过交互式输入,比如命令中包含 -p xxx 会报错。

可选交互式官方文档:https://picocli.info/#_optionally_interactive

让我们测试一下,给上面的示例代码输入以下参数:

new CommandLine(new Login()).execute("-u""user123""-p""xxx""-cp");

执行效果如下图,出现了参数不匹配的报错:

官方提供了可选交互式的解决方案,通过调整 @Option 注解中的 arity 属性来指定每个选项可接受的参数个数,就能解决这个问题。

arity 官方介绍:https://picocli.info/#_arity

示例代码如下:

@Option(names = {"-p""--password"}, arity = "0..1", description = "Passphrase", interactive = true)
String password;

然后可以直接在完整命令中给交互式选项设置值:

new CommandLine(new Login()).execute("-u""user123""-p""123""-cp");

执行结果如图,不再提示让用户输入 password 选项,而是直接读取了命令中的值:

这里鱼皮推荐一个最佳实践:建议给所有需要交互式输入的选项都增加 arity 参数(一般是 arity = "0..1"),这样用户既可以在完整命令中直接给选项填充参数,也可以选择交互式输入。

示例代码如下:

public class Login implements Callable<Integer{
    @Option(names = {"-u""--user"}, description = "User name")
    String user;

    // 设置了 arity 参数,可选交互式
    @Option(names = {"-p""--password"}, arity = "0..1", description = "Passphrase", interactive = true)
    String password;

    // 设置了 arity 参数,可选交互式
    @Option(names = {"-cp""--checkPassword"}, arity = "0..1", description = "Check Password", interactive = true)
    String checkPassword;

    public Integer call() throws Exception {
        System.out.println("password = " + password);
        System.out.println("checkPassword = " + checkPassword);
        return 0;
    }

    public static void main(String[] args) {
        new CommandLine(new Login()).execute("-u""user123""-p""123""-cp""456");
    }
}
4)强制交互式

在之前已经提到,如果用户不在命令中输入交互式选项(比如 -p),那么系统不会提示用户输入这个选项,属性的值将为默认值(比如 null)。

举个例子,下列命令中不带 -p 选项:

new CommandLine(new Login()).execute("-u""user123");

执行就会发现,程序不会提示用户输入 -p 选项的参数,而是直接输出结果,值为 null:

但有些时候,我们要求用户必须输入某个选项,而不能使用默认的空值,怎么办呢?

官方给出了强制交互式的解决方案,参考文档:https://picocli.info/#_forcing_interactive_input

但是,官方的解决方案是需要自己定义业务逻辑的。原理是在命令执行后对属性进行判断,如果用户没有输入指定的参数,那么再通过 System.console().readLine 等方式提示用户输入,示例代码如下:

@Command
public class Main implements Runnable {
    @Option(names = "--interactive", interactive = true)
    String value;

    public void run() {
        if (value == null && System.console() != null) {
            // 主动提示用户输入
            value = System.console().readLine("Enter value for --interactive: ");
        }
        System.out.println("You provided value '" + value + "'");
    }

    public static void main(String[] args) {
        new CommandLine(new Main()).execute(args);
    }
}

个人不是很喜欢这种方案,因为要额外编写提示代码,感觉又回到自主实现了。

鱼皮想出的一种方案是,编写一段通用的校验程序,如果用户的输入命令中没有包含交互式选项,那么就自动为输入命令补充该选项即可,这样就能强制触发交互式输入。

说通俗一点,检测 args 数组中是否存在对应选项,不存在则为数组增加选项元素。

该思路作为一个小扩展点,实现起来并不复杂,大家可以自行实现。(小提示:可以利用反射自动读取必填的选项名称)

4、子命令

子命令是指命令中又包含一组命令,相当于命令的分组嵌套,适用于功能较多、较为复杂的命令行程序,比如 git、docker 命令等。

官方文档:https://picocli.info/#_subcommands

在 Picocli 中,提供了两种设置子命令的方式。

1)声明式

通过 @Command 注解的 subcommands 属性来给命令添加子命令,优点是更直观清晰。

示例代码如下:

@Command(subcommands = {
    GitStatus.class,
    GitCommit.class,
    GitAdd.class,
    GitBranch.class,
    GitCheckout.class,
    GitClone.class,
    GitDiff.class,
    GitMerge.class,
    GitPush.class,
    GitRebase.class,
    GitTag.class
})
public class Git 
/* ... */ }
2)编程式

在创建 CommandLine 对象时,调用 addSubcommand 方法来绑定子命令,优点是更灵活。

示例代码如下:

CommandLine commandLine = new CommandLine(new Git())
        .addSubcommand("status",   new GitStatus())
        .addSubcommand("commit",   new GitCommit())
        .addSubcommand("add",      new GitAdd())
        .addSubcommand("branch",   new GitBranch())
        .addSubcommand("checkout"new GitCheckout())
        .addSubcommand("clone",    new GitClone())
        .addSubcommand("diff",     new GitDiff())
        .addSubcommand("merge",    new GitMerge())
        .addSubcommand("push",     new GitPush())
        .addSubcommand("rebase",   new GitRebase())
        .addSubcommand("tag",      new GitTag());
实践

让我们编写一个示例程序,支持增加、删除、查询 3 个子命令,并传入不同的 args 来测试效果。

完整代码如下:

package com.yupi.cli.example;

import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "main", mixinStandardHelpOptions = true)
public class SubCommandExample implements Runnable {

    @Override
    public void run() {
        System.out.println("执行主命令");
    }

    @Command(name = "add", description = "增加", mixinStandardHelpOptions = true)
    static class AddCommand implements Runnable {
        public void run() {
            System.out.println("执行增加命令");
        }
    }

    @Command(name = "delete", description = "删除", mixinStandardHelpOptions = true)
    static class DeleteCommand implements Runnable {
        public void run() {
            System.out.println("执行删除命令");
        }
    }

    @Command(name = "query", description = "查询", mixinStandardHelpOptions = true)
    static class QueryCommand implements Runnable {
        public void run() {
            System.out.println("执行查询命令");
        }
    }

    public static void main(String[] args) {
        // 执行主命令
        String[] myArgs = new String[] { };
        // 查看主命令的帮助手册
//        String[] myArgs = new String[] { "--help" };
        // 执行增加命令
//        String[] myArgs = new String[] { "add" };
        // 执行增加命令的帮助手册
//        String[] myArgs = new String[] { "add", "--help" };
        // 执行不存在的命令,会报错
//        String[] myArgs = new String[] { "update" };
        int exitCode = new CommandLine(new SubCommandExample())
                .addSubcommand(new AddCommand())
                .addSubcommand(new DeleteCommand())
                .addSubcommand(new QueryCommand())
                .execute(myArgs);
        System.exit(exitCode);
    }
}

测试运行,发现当输入 --help 参数时,打印出了主命令和所有的子命令信息,证明子命令绑定成功:

实践

编程导航星球的定制化代码生成项目就是使用了 Picocli 来开发命令行应用。

👉🏻 编程导航原创项目教程系列:https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd


OK 就到这里,创作不易,学会的同学点个赞吧~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/239368.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NAS入门(学习笔记)

文章目录 AutoMLNAS初期NAS当前NAS框架One-Shot NAS权重共享策略 Zero-Shot NASZen-NASNASWOTEPENAS 参考资料 AutoML 深度学习使特征学习自动化 AutoML 使深度学习自动化 自动化机器学习 (automated machine learning) 是一种自动化的数据驱动方法, 并做出一系列决策。 按…

【Docker】网络模式➕自定义网络

&#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Docker的相关操作吧 目录 &#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 一.Docker网络模式的介绍 二. 网桥模…

Server/PC 稳定性测试(Windows)

Server/PC 稳定性测试 1. CPU - 中央处理器1.1 CPU稳定性1.2 CPU温度 2. Memory - 内存2.1 内存性能 3. DISK - 硬盘3.1 HDD - 机械硬盘机械硬盘转速详解监测工具 3.2 SSD - 固态硬盘监测工具 4. 使用时长cpu能用多久&#xff1f;主板能用多久&#xff1f;内存能用多久&#xf…

博后期间各种计划

博后期间各种计划&#xff1a; &#xff08;1&#xff09;国家自然科学基金&#xff08;青年项目&#xff09;、&#xff08;2&#xff09;博士后创新人才支持计划&#xff08;23年新改革&#xff1a;28w*2生活补助经费8w科研启动经费&#xff1b;每年资助400多人&#xff1b;…

Ansible-剧本与变量

对于运维小伙伴来讲&#xff0c;Ansible并不陌生&#xff0c;配置简单&#xff0c;上手容易&#xff0c;只要掌握几个基本的模块就可以解决好多运维中重复的事&#xff0c;但是对于处理更为高级的功能和更大、更复杂的项目时&#xff0c;管理和维护Ansible Playbook或高效使用将…

解决MPICH的GPU初始化失败:一次深入探索

今天来分享“MPICH&#xff1a;MPII_Init_thread(222): gpu_init failed”这个问题的解决方式 文章目录 前言问题原因解决方案 前言 如果在安装MPICH的时候没有注意要一些选项&#xff0c;那么当使用mpicxx mpi_send.cpp -o send && mpirun -n 2 ./send进行编译输出的…

C++ 之LeetCode刷题记录(十一)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 向耗时0s前进。 67. 二进制求和 给你两个二进制字符串 a 和 b &#xff0c;以二进制字符串的形式返回它们的和。 示例 1&#xff1a; 输入…

最新AI绘画Midjourney绘画提示词Prompt大全

一、Midjourney绘画工具 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭…

抖音SEO搜索排名优化培训教程课件

【干货资料持续更新&#xff0c;以防走丢】 抖音SEO搜索排名优化培训教程课件 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音运营资料合集 &#xff08;完整资料包含以下内容&#xff09; 目录 抖音易爆单的商品特征 抖音作为一款短视频平台&#xff0c;…

基于杂交PSO算法的风光储微网日前优化调度(MATLAB实现)

微网中包含&#xff1a;风电、光伏、储能、微型燃气轮机&#xff0c;以最小化电网购电成本、光伏风机的维护成本、蓄电池充放电维护成本、燃气轮机运行成本及污染气体治理成本为目标&#xff0c;综合考虑&#xff1a;功率平衡约束、燃气轮机爬坡约束、电网交换功率约束、储能装…

C语言数组

文章目录 1:一维数组的创建和初始化1.1 创建方式1.2:一维数组的初始化1.3:一维数组的使用1.3.1:数组下标 1.4:一维数组在内存中的存储 2:二维数组的创建和初始化2.1&#xff1a;二维数组的创建2.2:二维数组的初始化2.3:二维数组的下标2.4:二维数组在内存中的存储 3:数组越界4:数…

request entity too large 解决请求实体过大问题的方法

在网络请求过程中&#xff0c;有时会出现请求实体过大而导致服务器无法处理的情况。本文将介绍两种情况及其解决办法&#xff0c;真实可用&#xff01; 问题描述 请求实体过大问题主要分为两种情况&#xff1a; 1、带413状态码的请求实体过大 这种情况通常发生在请求文件过…

display布局实现一侧的盒子高度与另一侧盒子的高度等高

实现两边容器的高度等高主要是用 align-items: stretch 这个属性 stretch 拉伸: 子元素没有高度或高度为auto&#xff0c;将占满整个容器的高度 <template><div><h3>我是测试页面</h3><div class"container"><div class"left-…

09、Kafka ------ 通过修改保存时间来删除消息(retention.ms 配置)

目录 通过修改保存时间来删除消息★ 删除指定主题的消息演示1、修改kafka检查过期消息的时间间隔2、修改主题下消息的过期时间3、查看修改是否生效4、先查看下主题下有没有消息5、添加几条消息看效果6、查看消息是否被删除 ★ 恢复主题的retention.ms配置1、先查看没修改前的te…

常见半导体设备厂商介绍

半导体作为全球最重要的一个产业&#xff0c;每年为全球经济贡献数千亿美元产值。在整个产业链上&#xff0c;除我们耳熟能详的英特尔、AMD、高通、台积电等生产商外&#xff0c;还包括了众多著名的材料商和设备商。今天我们将对全球最为出色的半导体设备厂商进行一次盘点和介绍…

学习c语言,奇偶排序

如果左边是奇数右边是偶数就不管他&#xff0c;如果左边找到偶数右边是奇数则互相交换。

【大厂算法面试冲刺班】day0:数据范围反推时间复杂度

常见算法的时间复杂度 规定n是数组的长度/树或图的节点数 二分查找&#xff1a;O(logn) 双指针/滑动窗口&#xff1a;O(n) DFS/BFS&#xff1a;O(n) 构建前缀和&#xff1a;O(n) 查找前缀和&#xff1a;O(1) 一维动态规划&#xff1a;O(n) 二维动态规划&#xff1a;O(n^2) 回溯…

图像表示方法

RGB表示 RGB是使用三基色合成的原理&#xff0c;我们看到的彩色图片&#xff0c;都有三个通道&#xff0c;分别为红、绿、蓝通道&#xff0c;如果需要透明度则还有alpha分量. 通常每个通道用8bit表示&#xff0c;8bit能表示256种颜色&#xff0c;所以可以组成 256256256167772…

云服务器搭建GitLab

经验总结&#xff1a; 1、配置需求&#xff1a;云服务器内存最低4G 2、内存4G的云服务器&#xff0c;在运行容器后&#xff0c;会遇到云服务器操作卡顿问题&#xff0c;这里有解决方案 转载&#xff1a;服务器搭建Gitlab卡顿解决办法-CSDN博客 3、云服务器的操作系统会影响…

游戏开发,中小公司跳槽去大厂容易还是考研应届生校招容易?

游戏开发&#xff0c;中小公司跳槽去大厂容易还是考研应届生校招容易&#xff1f; 在之前的文章中&#xff0c;我们提到过&#xff0c;游戏开发行业首选直接进入游戏大厂。《开发者必读&#xff1a;如何选择适合的游戏开发公司&#xff1f;》因为大厂不仅能提供良好的职业发展…