これは、なにをしたくて書いたもの?
ちょっと前から、picocliというJavaでのコマンドラインアプリケーション向けのフレームワークがあるのが気になっていまして。
picocli - a mighty tiny command line interface
これまでJavaのコマンドラインアプリケーション向けのライブラリなどといえば、こちらを使っていました。
個人的にはargs4jをよく使っていましたね。
picocliもかなり良さそうなので、1度試しておこうかなと。
picocli
繰り返しますが、Javaのコマンドラインアプリケーション向けのフレームワークです。
picocli - a mighty tiny command line interface
GNU、POSIX、MS-DOSなどのコマンドライン構文のサポート、カラー、ヘルプの生成から、オプション、引数(パラメーター)、
サブコマンドも扱えるそうで。
さらに、GraalVMによるネイティブイメージの作成もサポートしているので、よりCLIフレンドリーですね。
ドキュメントがとても充実しているのもポイントで、かなり機能があることがよくわかります。
picocli - a mighty tiny command line interface
参考記事もGitHub上でリンクされていたり。
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」とすると行番号を表示するようにします。オプションに冗長感ありますけど、まずはパラメーターありのオプション
ということで。
引数やオプションなどはアノテーションで表現していくのですが、基本はこちらを見るとわかる気がします。
作成したソースコードは、こちら。
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以外の型でも受け取れます。
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;
また、引数の位置も指定するようにしてみます。入力となるファイルは、複数指定できないようにしました。また、ファイルを
指定しないと標準入力から読み込むような動きもしません。
@CommandLine.Parameters(index = "0", paramLabel = "PATTERN") String pattern; @CommandLine.Parameters(index = "1", paramLabel = "FILE") Path path;
その他は、そんなに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
サブコマンドを使う
作成するコマンドの最後として、サブコマンドを使ってみましょう。
先ほど作った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インターフェースを実装したりする
方法もあったり
オプション、引数のパース結果をもっと細かくコントロールする方法もあったりするようです。
どっちにしろ、機能がとても多くて書ききれないので、今回はこのくらいにしておきます。
ネイティブイメージを作る
最後に、GraalVMを使ってネイティブイメージを作ってみましょう。
Pluggable Annotation Processing APIを使ったモジュールがあるので、こちらをpom.xmlに設定します。
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を使います。
こんな感じで。
<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を作る時には、今度からこちらを活用していきましょう。