➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
客製化 JRE使用 JMH benchmark Java/Spring Boot app

Picocli:開發 Java CLI 工具

Table of contents

1 Picocli 用途

有時我地會想用寫一個冇 GUI 介面既 CLI 程式去重覆處理一啲野。舉個例,我地想批量處理圖片,為每張圖片加上類似美圖 apps 既濾鏡效果,而為左方便我地比較邊張既效果出到黎會好啲,我地要處理同一批圖片好幾次,每次都要用唔同既參數、配置。當然我地寫個 Java 程式,main method 裡面寫好曬會針對同一批圖片用上濾鏡一、濾鏡二、濾鏡三等等既處理,每次都開個 IDE 出黎直接改 code 裡面既參數,然後喺 IDE 度執行個程式。不過,假如呢個程式係要畀其他人用,咁就應該要整到似返個 CLI 既工具咁,可以以統一既操作方式,根據當刻用家提供既唔同參數而執行邏輯。仲有,通常 CLI 工具都會自帶內置既 user manual,話畀用家知道有咩參數可以用。
Picocli 正正係一個可以畀我地非常方便咁開發 Java CLI 工具既 Maven library。佢提供左唔少 annotations 畀我地定義 CLI 既 options、parameters、subcommand 等等,然後佢會幫我地喺 console output 度 print help(類似 man page 既 user manual)。

2 CLI 基礎知識

2.1 Options、positional parameters

一般黎講,CLI 既參數有兩種:
類別描述
OptionKey value pairs 或者 boolean(開/關)。
Positional parameter需要順次序提供既參數。

2.2 Options、positional parameters 既次序

我地只需要注意 positional parameters 既次序。
至於 options,一般都會將佢地擺先過 positional parameters,例如 kubectl exec -i -t --container=mycontainer mypod,呢度 exec 係 subcommand,後面以 - 或者 -- 開頭既都係 options,而最後既 mypod 係唯一既 positional parameters。
不過,我地亦可以將 options 擺喺成句 command 既最後,例如 kubectl logs -f mypod --tail=20,呢度我地就將 --tail option 放在喺 positional parameter mypod 既後面。

2.3 Double-dash

Double-dash(--)係用黎分隔 options 以及 positional parameters。
舉個例,如果個 positional parameter 以 - 開頭,就有可能被誤以為係一個 option,咁我地可以喺第一個 positional parameter 既前面加上 --

2.4 Subcommand

舉個 subcommand 既例子:
kubectl get pods -n my-namespace
以上既 getpods 就係 subcommand 以及 sub-subcommand 既例子。

3 Maven dependencies

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

4 基本例子

以下會展示最基本既例子。
MainCli.java
1public class MainCli { 2 3 public static void main(String[] args) { 4 5 final CommandLine cli = new CommandLine(new MyCommand()); 6 final int exitCode = cli.execute(args); 7 8 System.exit(exitCode); 9 } 10}
MyCommand.java
1@Command( 2 name = "my-cli-app", 3 description = "This app is a CLI tool for testing Picocli.", 4 sortOptions = false, 5 showDefaultValues = true, 6 mixinStandardHelpOptions = true, 7 showEndOfOptionsDelimiterInUsageHelp = true, 8 versionProvider = MyVersionProvider.class, 9) 10public class MyCommand implements Runnable { 11 12 @Option(/* name, description, required, defaultValue, etc */) 13 String sampleOption; 14 15 @Parameters(/* index, description, arity, etc */) 16 String sampleParameter; 17 18 @Override 19 public void run() { 20 // log.info(...); 21 } 22}
註:
  • MyCommand 係一個終點 command,所以需要 implements Runnable
  • 如果執行既時候畀左 --help option,佢就會自動生成類似 man page 既結果。
  • 如果執行既時候畀左 --version option 而又冇畀 --help option,佢就會用我地自定義既 MyVersionProvider class 黎生成結果。
  • 又齊曬 required 既 options 以及 positional parameters,咁佢就會 call 個 run() method。

5 探討 Picocli annotations

Picocli 既 @Command@Option@Parameters annotations 都有一個 description attribute,可以畀我地喺 --help 度詳述佢地既用途。
以下我地會透過重現 kubectl CLI 黎學習 Picocli。

5.1 @Command

5.1.1 Top-level command

1@Command( 2 name = "kubectl", 3 description = "This is a Picocli demo to replicate the 'kubectl' CLI.", 4 scope = ScopeType.INHERIT, 5 sortOptions = false, 6 showDefaultValues = true, 7 mixinStandardHelpOptions = true, 8 showEndOfOptionsDelimiterInUsageHelp = true, 9 versionProvider = MyVersionProvider.class, 10 subcommands = { 11 KubectlGetCommand.class, 12 KubectlDeleteCommand.class, 13 KubectlLogsCommand.class, 14 } 15) 16public class KubectlCommand { 17 // 唔計 --help、--version 呢啲特殊既 predefined options, 18 // 呢個本身唔係一個終點 command,所以唔洗 implements Runnable。 19}
Class definition:
  • 如果唔係一個終點 command(執行既時候一定要提供 subcommand),就唔洗 implements Runnable,因為就算定義左 run() method,佢都唔會 call。
  • 相反,如果係一個終點 command,就要 implements Runnable,因為佢會 call 我地定義既 run() method。
一啲重要既 attributes:
AttributeValue解釋
name冇所謂Top-level command 既 name attribute 冇用,因為我地執行既係個 CLI 既檔案名。
description冇所謂純粹 --help 顯示用。
scopeINHERITSubcommands 會 inherit 呢個 @Command annotation 既 attributes。
sortOptionsfalse--help 裡面既 options 以 Java class 裡面啲 fields 既 declaration order 黎排序(默認係以英文字母排序)。
showDefaultValuestrue--help 裡面顯示默認值。
mixinStandardHelpOptionstrue加入 --help-h--version-V 既 predefined options。
showEndOfOptionsDelimiterInUsageHelptrue--help 裡面顯示 -- 既用途。
versionProvider自定義 class用自定義既 class 黎生成 --version 既結果。
subcommands自定義 classesSubcommand 既自定義 classes。如果係終點 command,就要 implements Runnable

5.1.2 Subcommand

以下係一啲非終點 subcommand 既例子:
1@Command( 2 name = "get", 3 description = "This is the 'kubectl get' subcommand.", 4 subcommands = { 5 KubectlGetPodsCommand.class, 6 KubectlGetDeploymentsCommand.class, 7 } 8) 9public class KubectlGetCommand {}
1@Command( 2 name = "delete", 3 description = "This is the 'kubectl delete' command.", 4 subcommands = { 5 KubectlDeletePodsCommand.class, 6 KubectlDeleteDeploymentsCommand.class, 7 } 8) 9public class KubectlDeleteCommand {}
以下係一個終點 subcommand 既例子:
1@Slf4j 2@Command( 3 name = "logs", 4 description = "This is the 'kubectl logs' command." 5) 6public class KubectlLogsCommand implements Runnable { 7 8 @Option(names = { "-f", "--follow" }, required = false, defaultValue = "false") 9 boolean follow; 10 11 @Option(names = { "--tail" }, required = false, defaultValue = "-1") 12 int tail; 13 14 @Parameters(index = "0") 15 String podName; 16 17 @Override 18 public void run() { 19 log.info("kubectl logs"); 20 log.info("-f: {}", follow); 21 log.info("--tail: {}", tail); 22 log.info("Param 0: {}", podName); 23 } 24}
Command 例子:
java -jar kubectl.jar logs mypod java -jar kubectl.jar logs -f --tail=20 mypod

5.1.3 Sub-subcommand

以下都係終點 commands 既例子:
1@Command( 2 name = "pods", 3 aliases = { "po", "pod" }, 4 description = "This is the 'kubectl get pods' command." 5) 6public class KubectlGetPodsCommand implements Runnable { 7 8 @Override 9 public void run() { 10 System.out.println("kubectl get pods"); 11 } 12}
Command 例子:
java -jar kubectl.jar get po java -jar kubectl.jar get pod java -jar kubectl.jar get pods
1@Slf4j 2@Command( 3 name = "pods", 4 aliases = { "po", "pod" }, 5 description = "This is the 'kubectl delete pods' command." 6) 7public class KubectlDeletePodsCommand implements Runnable { 8 9 @Parameters( 10 index = "0", 11 description = "A list of pod names.", 12 arity = "1..*" 13 ) 14 List<String> podNames; 15 16 @Override 17 public void run() { 18 log.info("kubectl delete pods"); 19 log.info("Param 0: ({} elements) {}", podNames.size(), podNames); 20 } 21}
Command 例子:
java -jar kubectl.jar delete po pod1 java -jar kubectl.jar delete pod pod1 java -jar kubectl.jar delete pods pod1 java -jar kubectl.jar delete pods pod1 pod2

5.2 @Option(option)

5.2.1 必要 option

@Option( names = { "-i", "--input" }, required = true ) String input;

5.2.2 非必要 option

@Option( names = { "-o", "--output" }, required = false ) String output;

5.3 @Parameters(positional parameter)

例子:
@Parameters( index = "0", description = "1st parameter" ) String foo;
@Parameters( index = "1", description = "2nd parameter" ) String bar;
1@Parameters( 2 index = "2", 3 description = "3rd parameter (multiple values)", 4 arity = "1..*" 5) 6List<String> varargs;

5.3.1 非必要 positional parameter

我地可以設置 arity0..1,咁個 positional parameter 就可有可無。
@Parameter(index = "0", arity = "0..1") String foo;

6 Data types

6.1 String

@Option( names = { "--foo" }, defaultValue = "foo" // 任意值 ) String foo;

6.2 boolean

@Option( names = { "--foo" }, defaultValue = "true" // "true"、"false" ) boolean foo;

6.3 List<String>

Option、positional parameter 都可以做到 multiple values,用起上黎都係差唔多,默認係用空格分隔開多個值。
不過要注意既係,就算用左 array 或者 Collection types(例如 ListSet),我地都要額外設置 arity0..*1..* 之類(即係值既數量既上下限),去覆蓋佢對 multi-valued types 既 arity 默認值 1
除此之外,喺以下既情況下,我地需要設置 split 去話畀佢知我地會用咩符號黎分隔多個值:
  • 想用空格以外既符號去分隔多個值,例如 ,;
  • defaultValue 度提供多個值(如果冇設置 split 既話會當左做一個帶空格既值)。
註:
  • 如果有幾個 options 都係接受多個值,例如 --foo 1 2 3 --bar 4 5 6,佢會識得分個值係咪其中一個 option 黎,如果係(例子裡面既 --bar),佢會識得分返開佢地,而唔係當左佢係前面 option(例子裡面既 --foo)既其中一個值。
    • 即使唔係 - 或者 -- 開頭佢都會識分。
  • 如果有幾個 positional parameters 都接受多個值既話,測試過係會出問題,即使前面既已經設置左 arity 數量上限。
1@Option( 2 names = { "--foo" }, 3 defaultValue = "item1 item2 item3", 4 split = " " 5) 6List<String> foo;
Command 例子:
--foo item1 --foo item1 item2
以下既例子用 , 去分隔多個值:
1@Option( 2 names = { "--foo" }, 3 defaultValue = "item1,item2,item3", // 要冇 space 4 split = "," 5) 6List<String> foo;
Command 例子:
--foo item1 --foo item1,item2

6.4 enum

1public static enum MyEnum { 2 FOO, BAR 3} 4 5@Option( 6 names = { "--foo" }, 7 defaultValue = "BAR" // 個 enum 既其中一個 constant 8) 9MyEnum foo;

7 參考資料