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

news2024/11/16 7:02:48

大家好,我是鱼皮,对不会前端的同学来说,开发 命令行工具 是一种不错的展示系统功能的方式。在 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.coloradmin.cn/o/1397214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网工内推 | 网络工程师,CCNP认证优先,带薪年假,补贴多

01 极三科技 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1、门店网络运维 2、门店网络项目支持 3、配合店铺Support team处理网络相关问题 任职要求&#xff1a; 1、大专及以上相关学历 2、3年以上相关工作经验 3、精通Cisco 路由器&#xff0c;交换机配置 4…

2.4G收发芯片遥控玩具方案开发,解决方案

2.4G遥控玩具是指通过2.4GHz无线信号进行遥控的玩具。这种玩具通常包括遥控器和遥控车、挖机、飞机、船等各种类型的模型。使用2.4G频段的遥控器可以提供更稳定和可靠的信号传输&#xff0c;具有较长的控制距离和较低的干扰。相比之前使用的其他频段的遥控器&#xff0c;2.4G遥…

前台vue配置

前台 vue环境 1.傻瓜式安装node: 官网下载&#xff1a;https://nodejs.org/zh-cn/2.安装cnpm: >: npm install -g cnpm --registryhttps://registry.npm.taobao.org3.安装vue最新脚手架: >: cnpm install -g vue/cli注&#xff1a;如果2、3步报错&#xff0c;清除缓…

第11章 GUI Page500~504 步骤三十二:打开画板文件02

各个图元类新增GetTypeName_Static()&#xff0c;并将原来的GetTypeName()改为调用静态方法实现&#xff1a; 直线&#xff1a; 圆&#xff1a; 十字&#xff1a; 矩形&#xff1a; 文字&#xff1a; tool_4_save_load.hpp添加两行 tool_4_save_load.cpp增加&#xff1a; 增加…

2024--Django平台开发-订单项目管理(十四)

day14 订单管理系统 1.关于登录 1.1 UI美化 页面美化&#xff0c;用BootStrap 自定义BooStrapForm类实现。 class BootStrapForm:exclude_filed_list []def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)# {title:对象,"percent":对象}fo…

TestNG注释

目录 TestNG注释列表 BeforeXXX和AfterXXX注释放在超类上时如何工作&#xff1f; 使用BeforeXXX和AfterXXX TestNG注释 TestNG是一个测试框架&#xff0c;旨在简化广泛的测试需求&#xff0c;从单元测试&#xff08;隔离测试一个类&#xff09;到集成测试&#xff08;测试由…

SpringBoot——纯注解配置的Spring

1.环境搭建 1.1.创建工程 拷贝ssm工程&#xff1a; 1.2.待改造的问题 我们发现&#xff0c;之所以我们现在离不开xml配置文件&#xff0c;是因为我们有一处很关键的配置&#xff0c;如果他要也能用注解配置&#xff0c;那么我们就可以脱离xml文件了&#xff1a; 1.2.1.jdbc…

vuex前端开发,getters是什么?怎么调用?简单的案例操作

vuex前端开发,getters是什么&#xff1f;怎么调用&#xff1f;简单的案例操作&#xff01; 下面通过一些简单的案例&#xff0c;来了解一下&#xff0c;vuex当中的getters到底是什么意思&#xff0c;有哪些实际的操作案例。 Vuex的getters主要用于对store中的state进行计算或过…

使用jackson对java类中包含泛型属性的bean进行序列化和反序列化

最近在做项目时&#xff0c;需要对java的bean对象进行序列化转换为string字符串后&#xff0c;存入redis&#xff0c;然后再从redis中读取该string对象&#xff0c;反序列化为bean对象。正常的简单类型的对象使用jackson可以非常方便进行互为转换操作&#xff0c;但我们操作的对…

icu4c库中icudtl.dat文件剪裁

背景 在工程中需要把ansi编码转utf-8,引入icu4c库&#xff0c;默认编译出来的.dat文件有30多M&#xff0c;由于仅仅需在MacOS系统下要把Windows中的ansi编码转成utf-8&#xff0c;需要进行裁剪。 编译icu4c工程 源码下载&#xff1a;https://github.com/unicode-org/icu&…

HTML 属性

HTML 属性学习笔记 HTML 属性真的好神奇呢&#xff01;它们像是元素的额外说明&#xff0c;总是伴随着特定的格式出现在元素的开始标签里。接下来&#xff0c;我就记录下一些常见的 HTML 属性吧&#xff01; HTML 属性的基本概念 每个 HTML 元素都可以有自己的属性&#xff…

1739. 迷宫的所有路径-深度优先搜索-DFS

代码&#xff1a; #include<bits/stdc.h> using namespace std; int n; int fx[4]{0,1,0,-1}; int fy[4]{1,0,-1,0}; bool vis[100][100]; int q[35][3]; int c; void print(int k){c;cout<<c<<":";for(int i1;i<k;i){cout<<q[i][1]<…

测试 ASP.NET Core 中间件

正常情况下&#xff0c;中间件会在主程序入口统一进行实例化&#xff0c;这样如果想单独测试某一个中间件就很不方便&#xff0c;为了能测试单个中间件&#xff0c;可以使用 TestServer 单独测试。 这样便可以&#xff1a; 实例化只包含需要测试的组件的应用管道。发送自定义请…

多线程排序(java版)

&#x1f4d1;前言 本文主要是【排序】——多线程排序的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句&#x…

湖(岛屿)

from book&#xff1a;挑战程序设计竞赛

【Linux系统编程】程序地址空间

进程地址空间 进程地址空间是指每个进程在计算机内存中所占用的地址空间。地址空间是指能被访问的内存地址范围&#xff0c;它由若干个连续的内存块组成。每个进程都有自己的地址空间&#xff0c;这意味着每个进程都有自己的内存地址范围&#xff0c;不会与其他进程冲突。进程地…

回溯法:0-1背包问题

问题描述 给定种物品和一背包。 物品的重量是&#xff0c; 其价值为&#xff0c;背包的容量为 c。 问应该如何选择装入背包中的物品&#xff0c;使得装入背包中物品的总价值最大&#xff1f;注意物品不重复! 实例&#xff1a;物品价值V{12, 11, 9, 8}, 物品重量W{8, 6, 4, 3},…

操作系统-中断和异常(中断作用 类型 内外中断 中断机制原理)

文章目录 总览中断的作用中断类型内中断的例子外中断中断的分类中断机制的基本原理小结 总览 中断的作用 刚启动CPU上运行的是内核程序 中断类型 内中断的例子 当前执行指令非法是指参数有问题等其他原因 陷入指令是主动转换状态&#xff0c;但它并不是特权指令 外中断 时钟…

Docker(一)简介和基本概念:什么是 Docker?用它会带来什么样的好处?

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; 一、简介 本章将带领你进入 Docker 的世界。 什么是 Docker&#xff1f; 用它会带来什么样的好处&#xff1f; 好吧&#xff0c;让我们带…