CLOVER🍀

That was when it all began.

Javaのコマンドラインアプリケーション向けのフレームワーク、picocliで遊ぶ

これは、なにをしたくて書いたもの?

ちょっと前から、picocliというJavaでのコマンドラインアプリケーション向けのフレームワークがあるのが気になっていまして。

picocli - a mighty tiny command line interface

これまでJavaコマンドラインアプリケーション向けのライブラリなどといえば、こちらを使っていました。

args4j parent - Args4j

Commons CLI – Home

個人的にはargs4jをよく使っていましたね。

picocliもかなり良さそうなので、1度試しておこうかなと。

picocli

繰り返しますが、Javaコマンドラインアプリケーション向けのフレームワークです。

picocli - a mighty tiny command line interface

GitHub - remkop/picocli: Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.

GNUPOSIXMS-DOSなどのコマンドライン構文のサポート、カラー、ヘルプの生成から、オプション、引数(パラメーター)、
サブコマンドも扱えるそうで。

さらに、GraalVMによるネイティブイメージの作成もサポートしているので、よりCLIフレンドリーですね。

ドキュメントがとても充実しているのもポイントで、かなり機能があることがよくわかります。

picocli - a mighty tiny command line interface

picocli 4.2.0 API

参考記事もGitHub上でリンクされていたり。

Articles & Presentations

Articles & Presentations / 日本語

説明はこれくらいにして、試していってみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07)
OpenJDK 64-Bit Server VM GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.6, vendor: Oracle Corporation, runtime: /usr/local/graalvm-ce
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.3.0-40-generic", arch: "amd64", family: "unix"

ネイティブイメージも作ろうと思うので、GraalVMを使います。

準備とお題

まずはシンプルに。Maven依存関係はこちら。

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

お題ですが、すごく簡易なcat、grepコマンドを作り、さらにcat、grepをサブコマンドにしたランチャーを作ります。最後にネイティブ
イメージを作って完了としましょうか。

Getting Started的に

簡単なCatコマンドをマネたものを作ってみましょう。

引数としてファイルを任意数指定可能、ファイルを指定しなかったら標準入力から読み、オプションとしては「-n」の代わりに
「-p true」とすると行番号を表示するようにします。オプションに冗長感ありますけど、まずはパラメーターありのオプション
ということで。

引数やオプションなどはアノテーションで表現していくのですが、基本はこちらを見るとわかる気がします。

Options and Parameters

作成したソースコードは、こちら。
src/main/java/org/littlewings/picocli/Cat.java

package org.littlewings.picocli;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Stream;

import picocli.CommandLine;

@CommandLine.Command(name = "cat")
public class Cat implements Callable<Integer> {
    @CommandLine.Option(names = {"-h", "--help"}, description = "show this help", usageHelp = true)
    boolean showHelp;

    @CommandLine.Option(names = {"-p", "--print-line-number"}, paramLabel = "true or false", description = "number all output lines", defaultValue = "false")
    String printLineNumber;

    @CommandLine.Parameters(paramLabel = "FILE")
    List<Path> paths;

    int lineCount = 1;

    @Override
    public Integer call() throws Exception {
        if (paths != null) {
            for (Path path : paths) {
                try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
                    if ("true".equals(printLineNumber)) {
                        lines.forEach(line -> System.out.printf("%d: %s%n", lineCount++, line));
                    } else {
                        lines.forEach(line -> System.out.printf("%s%n", line));
                    }
                }
            }
        } else {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
            if ("true".equals(printLineNumber)) {
                reader.lines().forEach(line -> System.out.printf("%d: %s%n", lineCount++, line));
            } else {
                reader.lines().forEach(line -> System.out.printf("%s%n", line));
            }

        }

        return 0;
    }

    public static void main(String... args) {
        System.exit(new CommandLine(new Cat()).execute(args));
    }
}

Callableを実装したクラスを作成。型パラメーターには、Integerを指定。callの戻り値をコマンドの終了ステータスとして使います。

@CommandLine.Command(name = "cat")
public class Cat implements Callable<Integer> {

@Commandアノテーションを付与して、nameを「cat」とします。このnameの値が、サブコマンドやヘルプで使われます。

@Optionアノテーションで文字通りオプションを、@Parametersでコマンド引数を表します。

    @CommandLine.Option(names = {"-h", "--help"}, description = "show this help", usageHelp = true)
    boolean showHelp;

    @CommandLine.Option(names = {"-p", "--print-line-number"}, paramLabel = "true or false", description = "number all output lines", defaultValue = "false")
    String printLineNumber;

    @CommandLine.Parameters(paramLabel = "FILE")
    List<Path> paths;

アノテーションを付与したフィールドには、オプションや引数で指定した値が入ります。

String以外の型でも受け取れます。

Strongly Typed Everything

Listや配列などを使うと、複数個指定できるオプションや引数として使えわれます。

@Optionで「usageHelp」をtrueにすると、ヘルプオプションとして扱われます。

あとはcallメソッドの中を実装して

    @Override
    public Integer call() throws Exception {
        if (paths != null) {
            for (Path path : paths) {
                try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
                    if ("true".equals(printLineNumber)) {
                        lines.forEach(line -> System.out.printf("%d: %s%n", lineCount++, line));
                    } else {
                        lines.forEach(line -> System.out.printf("%s%n", line));
                    }
                }
            }
        } else {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
            if ("true".equals(printLineNumber)) {
                reader.lines().forEach(line -> System.out.printf("%d: %s%n", lineCount++, line));
            } else {
                reader.lines().forEach(line -> System.out.printf("%s%n", line));
            }

        }

        return 0;
    }

mainメソッドを作成。

    public static void main(String... args) {
        System.exit(new CommandLine(new Cat()).execute(args));
    }

このCallableのインスタンスを、CommandLineのコンストラクタに渡してexecuteを呼び出します。この時に、mainメソッドの引数を
渡すと@Optionや@Parametersアノテーションを付与したフィールドに値が入った状態でcallメソッドが呼び出されます。

executeの戻り値は、callメソッドの戻り値になります。

では、試してみましょう。

サンプルのファイルを作成。

$ python3 -c 'import this' > zen_of_python.txt

実行。大着してますが、「mvn exec:java」で実行します。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Cat -Dexec.args='zen_of_python.txt'

〜省略〜

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

行番号を表示してみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Cat -Dexec.args='-p true zen_of_python.txt'

〜省略〜

1: The Zen of Python, by Tim Peters
2: 
3: Beautiful is better than ugly.
4: Explicit is better than implicit.
5: Simple is better than complex.
6: Complex is better than complicated.
7: Flat is better than nested.
8: Sparse is better than dense.
9: Readability counts.
10: Special cases aren't special enough to break the rules.
11: Although practicality beats purity.
12: Errors should never pass silently.
13: Unless explicitly silenced.
14: In the face of ambiguity, refuse the temptation to guess.
15: There should be one-- and preferably only one --obvious way to do it.
16: Although that way may not be obvious at first unless you're Dutch.
17: Now is better than never.
18: Although never is often better than *right* now.
19: If the implementation is hard to explain, it's a bad idea.
20: If the implementation is easy to explain, it may be a good idea.
21: Namespaces are one honking great idea -- let's do more of those!

OKそうですね。

もうひとつファイルを増やしてみましょう。

$ lsb_release -a > ubuntu_version.txt

複数のファイルを対象にしてみます。オプションは、ロングオプションで指定してみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Cat -Dexec.args='--print-line-number true zen_of_python.txt ubuntu_version.txt'

〜省略〜

1: The Zen of Python, by Tim Peters
2: 
3: Beautiful is better than ugly.
4: Explicit is better than implicit.
5: Simple is better than complex.
6: Complex is better than complicated.
7: Flat is better than nested.
8: Sparse is better than dense.
9: Readability counts.
10: Special cases aren't special enough to break the rules.
11: Although practicality beats purity.
12: Errors should never pass silently.
13: Unless explicitly silenced.
14: In the face of ambiguity, refuse the temptation to guess.
15: There should be one-- and preferably only one --obvious way to do it.
16: Although that way may not be obvious at first unless you're Dutch.
17: Now is better than never.
18: Although never is often better than *right* now.
19: If the implementation is hard to explain, it's a bad idea.
20: If the implementation is easy to explain, it may be a good idea.
21: Namespaces are one honking great idea -- let's do more of those!
22: Distributor ID: Ubuntu
23: Description:    Ubuntu 18.04.4 LTS
24: Release:    18.04
25: Codename:   bionic

ファイルを指定せずに、標準出力から入力してみましょう。

$ cat ubuntu_version.txt | mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Cat

〜省略〜

Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

最後に、ヘルプを表示。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Cat -Dexec.args='-h'

〜省略〜

Usage: cat [-h] [-p=true or false] [FILE...]
      [FILE...]
  -h, --help      show this help
  -p, --print-line-number=true or false
                  number all output lines

ちなみに、このヘルプはカラーで見れます。

OKですね。

もうひとつサンプルを

続いて、grepもどきを作成してみましょう。

ソースコードは、こちら。
src/main/java/org/littlewings/picocli/Grep.java

package org.littlewings.picocli;

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import picocli.CommandLine;

@CommandLine.Command(name = "grep")
public class Grep implements Callable<Integer> {
    @CommandLine.Option(names = {"-h", "--help"}, description = "show this help", usageHelp = true)
    boolean showHelp;

    @CommandLine.Option(names = {"-n", "--line-number"}, description = "display match line number", defaultValue = "false")
    boolean printLineNumber;

    @CommandLine.Option(names = {"-v", "--invert-match"}, description = "print invert match")
    boolean printInvertMatch;

    @CommandLine.Parameters(index = "0", paramLabel = "PATTERN")
    String pattern;

    @CommandLine.Parameters(index = "1", paramLabel = "FILE")
    Path path;

    @Override
    public Integer call() throws Exception {
        Pattern p = Pattern.compile(pattern);

        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;
            int lineCount = 1;

            while ((line = reader.readLine()) != null) {
                lineCount++;

                Matcher m = p.matcher(line);

                boolean match = m.find();

                if (match && !printInvertMatch) {
                    if (printLineNumber) {
                        System.out.printf("%d: %s%n", lineCount, line);
                    } else {
                        System.out.printf("%s%n", line);
                    }
                } else if (!match && printInvertMatch) {
                    if (printLineNumber) {
                        System.out.printf("%d: %s%n", lineCount, line);
                    } else {
                        System.out.printf("%s%n", line);
                    }
                }
            }
        }

        return 0;
    }

    public static void main(String... args) {
        System.exit(new CommandLine(new Grep()).execute(args));
    }
}

先ほどと違うのは、booleanのオプションを設けて、指定すればtrueとなるようにしました。オプションとしては、「-n」と「-v」を定義
しています。

    @CommandLine.Option(names = {"-n", "--line-number"}, description = "display match line number", defaultValue = "false")
    boolean printLineNumber;

    @CommandLine.Option(names = {"-v", "--invert-match"}, description = "print invert match")
    boolean printInvertMatch;

Options and Parameters

また、引数の位置も指定するようにしてみます。入力となるファイルは、複数指定できないようにしました。また、ファイルを
指定しないと標準入力から読み込むような動きもしません。

    @CommandLine.Parameters(index = "0", paramLabel = "PATTERN")
    String pattern;

    @CommandLine.Parameters(index = "1", paramLabel = "FILE")
    Path path;

Positional Parameters

その他は、そんなにcatの例と変わらないので、説明は省略。

確認。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Grep -Dexec.args='better zen_of_python.txt'

〜省略〜

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Now is better than never.
Although never is often better than *right* now.

行番号を出力。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Grep -Dexec.args='-n better zen_of_python.txt'

〜省略〜

4: Beautiful is better than ugly.
5: Explicit is better than implicit.
6: Simple is better than complex.
7: Complex is better than complicated.
8: Flat is better than nested.
9: Sparse is better than dense.
18: Now is better than never.
19: Although never is often better than *right* now.

いわゆる「-v」をロングオプションで指定。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Grep -Dexec.args='-n --invert-match better zen_of_python.txt'

〜省略〜

2: The Zen of Python, by Tim Peters
3: 
10: Readability counts.
11: Special cases aren't special enough to break the rules.
12: Although practicality beats purity.
13: Errors should never pass silently.
14: Unless explicitly silenced.
15: In the face of ambiguity, refuse the temptation to guess.
16: There should be one-- and preferably only one --obvious way to do it.
17: Although that way may not be obvious at first unless you're Dutch.
20: If the implementation is hard to explain, it's a bad idea.
21: If the implementation is easy to explain, it may be a good idea.
22: Namespaces are one honking great idea -- let's do more of those!

ヘルプ。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Grep -Dexec.args='--help'

〜省略〜

Usage: grep [-hnv] PATTERN FILE
      PATTERN
      FILE
  -h, --help           show this help
  -n, --line-number    display match line number
  -v, --invert-match   print invert match

サブコマンドを使う

作成するコマンドの最後として、サブコマンドを使ってみましょう。

Subcommands

先ほど作ったcat、grepをサブコマンドにしたものを作ってみます。

こんな感じで。 src/main/java/org/littlewings/picocli/Launcher.java

package org.littlewings.picocli;

import java.util.concurrent.Callable;

import picocli.CommandLine;

@CommandLine.Command(name = "launcher", subcommands = {Cat.class, Grep.class})
public class Launcher implements Callable<Integer> {
    @CommandLine.Option(names = {"-h", "--help"}, description = "show this help", usageHelp = true)
    boolean showHelp;

    @Override
    public Integer call() throws Exception {
        return 0;
    }

    public static void main(String... args) {
        System.exit(new CommandLine(new Launcher()).execute(args));
    }
}

@Commandアノテーションのsubcommandsに、先ほどまで作成していたコマンドを、サブコマンドとして登録します。

@CommandLine.Command(name = "launcher", subcommands = {Cat.class, Grep.class})
public class Launcher implements Callable<Integer> {

callメソッドの中身は、特になにもなくても動きます。

とりあえず、ヘルプを見てみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Launcher -Dexec.args='-h'

〜省略〜

Usage: launcher [-h] [COMMAND]
  -h, --help   show this help
Commands:
  cat
  grep

ヘルプでサブコマンドが見れますね。

サブコマンドで「grep」を指定して、ヘルプを見てみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Launcher -Dexec.args='grep -h'

〜省略〜

Usage: launcher grep [-hnv] PATTERN FILE
      PATTERN
      FILE
  -h, --help           show this help
  -n, --line-number    display match line number
  -v, --invert-match   print invert match

grepのヘルプが見れますね。

もちろん、サブコマンドを指定して実行もできます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.picocli.Launcher -Dexec.args='grep -n Python zen_of_python.txt'

〜省略〜

2: The Zen of Python, by Tim Peters

補足

今回はコマンドの中身をCallable#callとして実装しましたが、その他にもRunnableとIExitCodeGeneratorインターフェースを実装したりする
方法もあったり

Executing Commands

オプション、引数のパース結果をもっと細かくコントロールする方法もあったりするようです。

DIY Command Execution

どっちにしろ、機能がとても多くて書ききれないので、今回はこのくらいにしておきます。

ネイティブイメージを作る

最後に、GraalVMを使ってネイティブイメージを作ってみましょう。

GraalVM Native Image

Pluggable Annotation Processing APIを使ったモジュールがあるので、こちらをpom.xmlに設定します。

Annotation Processor

Maven Compiler Pluginの、annotationProcessorPathsとして指定します。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>info.picocli</groupId>
                            <artifactId>picocli-codegen</artifactId>
                            <version>4.2.0</version>
                        </path>
                    </annotationProcessorPaths>
                    <compilerArgs>
                        <arg>-Aproject=${groupId}/${artifactId}</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

この状態でビルドすると

$ mvn compile

「compilerArgs」で指定したパス(「META-INF/native-image/picocli-generated」配下ですが)に、ファイルが生成されます。

$ find target/classes/META-INF/native-image/picocli-generated/org.example/picocli-example -type f
target/classes/META-INF/native-image/picocli-generated/org.example/picocli-example/proxy-config.json
target/classes/META-INF/native-image/picocli-generated/org.example/picocli-example/reflect-config.json
target/classes/META-INF/native-image/picocli-generated/org.example/picocli-example/resource-config.json

今回は、リフレクションに関する設定だけ、意味のある値が生成されました。
target/classes/META-INF/native-image/picocli-generated/org.example/picocli-example/reflect-config.json

[
  {
    "name" : "org.littlewings.picocli.Cat",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
      { "name" : "paths" },
      { "name" : "printLineNumber" },
      { "name" : "showHelp" }
    ]
  },
  {
    "name" : "org.littlewings.picocli.Grep",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
      { "name" : "path" },
      { "name" : "pattern" },
      { "name" : "printInvertMatch" },
      { "name" : "printLineNumber" },
      { "name" : "showHelp" }
    ]
  },
  {
    "name" : "org.littlewings.picocli.Launcher",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
      { "name" : "showHelp" }
    ]
  }
]

あとは、GraalVMのNative Image Maven Pluginを使います。

Integration with Maven

こんな感じで。

                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>19.3.1</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <skip>false</skip>
                            <imageName>cat</imageName>
                            <buildArgs>
                                --no-fallback
                            </buildArgs>
                            <mainClass>org.littlewings.picocli.Cat</mainClass>
                        </configuration>
                    </plugin>
                </plugins>

コマンドが3つあるので、今回はProfileで分けました。

    <profiles>
        <profile>
            <id>native-cat</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>19.3.1</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <skip>false</skip>
                            <imageName>cat</imageName>
                            <buildArgs>
                                --no-fallback
                            </buildArgs>
                            <mainClass>org.littlewings.picocli.Cat</mainClass>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>native-grep</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>19.3.1</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <skip>false</skip>
                            <imageName>grep</imageName>
                            <buildArgs>
                                --no-fallback
                            </buildArgs>
                            <mainClass>org.littlewings.picocli.Grep</mainClass>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>native-launcher</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>19.3.1</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <skip>false</skip>
                            <imageName>launcher</imageName>
                            <buildArgs>
                                --no-fallback
                            </buildArgs>
                            <mainClass>org.littlewings.picocli.Launcher</mainClass>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

では、ビルド。

$ mvn -P native-cat package && mvn -P native-grep package && mvn -P native-launcher package

確認。

$ ./target/cat -p true zen_of_python.txt 
1: The Zen of Python, by Tim Peters
2: 
3: Beautiful is better than ugly.
4: Explicit is better than implicit.
5: Simple is better than complex.
6: Complex is better than complicated.
7: Flat is better than nested.
8: Sparse is better than dense.
9: Readability counts.
10: Special cases aren't special enough to break the rules.
11: Although practicality beats purity.
12: Errors should never pass silently.
13: Unless explicitly silenced.
14: In the face of ambiguity, refuse the temptation to guess.
15: There should be one-- and preferably only one --obvious way to do it.
16: Although that way may not be obvious at first unless you're Dutch.
17: Now is better than never.
18: Although never is often better than *right* now.
19: If the implementation is hard to explain, it's a bad idea.
20: If the implementation is easy to explain, it may be a good idea.
21: Namespaces are one honking great idea -- let's do more of those!


$ ./target/grep -v Ubuntu ubuntu_version.txt 
Release:    18.04
Codename:   bionic


$ ./target/launcher --help
Usage: launcher [-h] [COMMAND]
  -h, --help   show this help
Commands:
  cat
  grep

OKですね。

まとめ

Javaコマンドラインアプリケーション向けのフレームワークである、picocliを試してみました。

最初はドキュメントの量を見てすごいなーと思いましたが、わかりやすく書かれていてとっつきやすかったと思います。

GraalVMによる、ネイティブイメージの作成に対応しているところもよいですね。

CLIを作る時には、今度からこちらを活用していきましょう。