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 既參數有兩種:
類別 | 描述 |
---|
Option | Key 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
以上既 get
、pods
就係 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:
Attribute | Value | 解釋 |
---|
name | 冇所謂 | Top-level command 既 name attribute 冇用,因為我地執行既係個 CLI 既檔案名。 |
description | 冇所謂 | 純粹 --help 顯示用。 |
scope | INHERIT | Subcommands 會 inherit 呢個 @Command annotation 既 attributes。 |
sortOptions | false | 令 --help 裡面既 options 以 Java class 裡面啲 fields 既 declaration order 黎排序(默認係以英文字母排序)。 |
showDefaultValues | true | 令 --help 裡面顯示默認值。 |
mixinStandardHelpOptions | true | 加入 --help /-h 、--version /-V 既 predefined options。 |
showEndOfOptionsDelimiterInUsageHelp | true | 令 --help 裡面顯示 -- 既用途。 |
versionProvider | 自定義 class | 用自定義既 class 黎生成 --version 既結果。 |
subcommands | 自定義 classes | Subcommand 既自定義 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
我地可以設置 arity
做 0..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(例如 List
、Set
),我地都要額外設置 arity
做 0..*
、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 參考資料