Spring Shell とは
- REPL ツールを簡単に作れるようにするためのフレームワーク
- Web アプリのようなリッチな UI が必要ない場合で、インタラクティブな CUI のみで十分な場合などに利用できる
- ベースには JLine が使用されている(jshell でも利用されているライブラリ)
環境
OS
Windows 10
Java
1.8.0_162
Hello World
実装
build.gradle
buildscript {
ext {
springBootVersion = '2.0.0.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.shell:spring-shell-starter:2.0.0.RELEASE')
}
bootJar {
baseName = 'sample'
}
SampleApplication.java
package sample.spring.shell;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello Spring Shell!!");
}
}
実行結果
> gradle build
> java -jar build\libs\sample.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.8.RELEASE)
(中略)
shell:>【hello】
Hello Spring Shell!!
shell:>【exit】
(略)
>
※Spring Shell 起動後の 【】
で括っている部分(hello
, exit
)はキー入力していることを表している
説明
- Spring Boot の依存について
- Spring Shell の、 2018 年 3 月現在の最新は 2.0.0
- ver 1 のことは知らないが、 ver 2 からは Spring Boot との統合が進められているらしく、公式ドキュメントも Spring Boot を使った作成方法を説明している
- 一応 Spring Boot が必須というわけではないらしいが、 Spring Boot を使わない方法が見当たらなかったので、ここでも Spring Boot を使った構築方法をメモする
- 作成された jar を実行すると、対話形式のシェルが起動する
- 自作したコマンドや、 Spring Shell がデフォルトで用意しているコマンドが使用できる
-
exit
でシェルを終了できる
組み込みのコマンド
コマンド | 説明 |
---|---|
clear |
現在シェルに表示されている内容を消す |
exit , quit
|
シェルを終了する |
help |
ヘルプを表示する |
script |
ファイルからコマンドを読み込んで実行する |
stacktrace |
最後に発生したエラーのスタックトレースを表示する |
helpを実行したときの様子
shell:>help
AVAILABLE COMMANDS
Built-In Commands
clear: Clear the shell screen.
exit, quit: Exit the shell.
help: Display help about available commands.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
Sample Commands
hello: Hello World
シェルの基本操作
ショートカット
- bash と同じようなショートカットが使える
ショートカット | 操作内容 |
---|---|
Ctrl + u
|
カーソルから左を削除 |
Ctrl + k
|
カーソルから右を削除 |
Ctrl + a
|
行頭へカーソルを移動 |
Ctrl + e
|
行末へカーソルを移動 |
Ctrl + w
|
1つ前の単語までを削除 |
Ctrl + d
|
カーソル位置の文字を削除 |
Ctrl + f
|
カーソルを1つ進める |
Ctrl + b
|
カーソルを1つ戻す |
Alt + f
|
カーソルを1単語進める |
Alt + b
|
カーソルを1単語戻す |
空白スペースを含む値を渡す
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class GreetingCommands {
@ShellMethod("Hello World")
public void hello(String text) {
System.out.println(text);
}
}
実行結果
shell:>【hello "foo bar"】
foo bar
shell:>【hello 'foo bar'】
foo bar
shell:>【hello "foo 'bar'"】
foo 'bar'
shell:>【hello 'foo "bar"'】
foo "bar"
shell:>【hello "foo \"bar\""】
foo "bar"
shell:>【hello 'foo \'bar\''】
foo 'bar'
- 空白スペースを含む文字列を引数に渡したい場合は、シングルクォーテーション (
'
) またはダブルクォーテーション ("
) で文字列を括る - シングルクォーテーションの中ではダブルクォーテーションがそのまま利用でき、ダブルクォーテーションの中ではシングルクォーテーションがそのまま利用できる
- シングルクォーテーションの中でシングルクォーテーションを、ダブルクォーテーションの中でダブルクォーテーションを使用したい場合は、バックスラッシュ(
\
)でエスケープする
空白スペースをエスケープする
shell:>【hello foo\ bar】
foo bar
- クォーテーションで括らなくても、空白スペース自体をエスケープする方法もある
複数行の入力
実行結果
shell:>【hello "abc】
dquote> 【defg】
dquote> 【hijk"】
abc defg hijk
- クォーテーションを開始した状態で改行を入れると、引き続き文字の入力が促されるようになる
- クォーテーションを閉じるまで、改行を含めた入力が1つの入力として処理される(改行自体は、最終的に空白スペースに置き換えられる)
Tab による入力補完
-
Tab
を入力すると様々な場所で入力補完が働くようになっている- 候補が表示されると、引き続き
Tab
を入力することで順番に選択肢にカーソルが移動する -
Enter
で候補を選択できる
- 候補が表示されると、引き続き
- コマンドの補完だけでなく、引数の補完も対応している
1つのコマンド入力に改行を含める
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class GreetingCommands {
@ShellMethod("Hello World")
public void hello(int a, int b, int c) {
System.out.println("a=" + a + ", b=" + b + ", c=" + c);
}
}
実行結果
shell:>【hello \】
> 【--a 10 \】
> 【--b 20 \】
> 【--c 30】
a=10, b=20, c=30
- バックスラッシュ (
\
) で区切ることで、1つのコマンドの入力を複数行に分けて記述できる
コマンドの定義
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello Spring Shell!!");
}
}
- コマンドを定義するには、まず任意のクラスを作成し
@ShellComponent
でクラスをアノテートする - 次にメソッドを作成して
@ShellMethod
でアノテートする-
@ShellMethod
のvalue
にはコマンドを説明する文言を設定する必要がある(設定しておかないと、起動時にエラーになる) - 説明の文言は、他のコマンドとの整合性を取るため次の条件を満たすように記述するのが良い
- 短い文章(1,2文程度)
- 大文字で始めて、ドットで終わらせる
-
- これで、メソッド名がそのままコマンド名になる
- メソッド名が
helloWorld
のように二単語以上のキャメルケースになっている場合は、hello-world
のようにハイフン区切りのコマンド名になる
- メソッド名が
- コマンドを実行すれば、対応するメソッドが実行されるようになる
コマンド名を指定する
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod(value="Hello World", key="hoge")
public void hello() {
System.out.println("Hello Spring Shell!!");
}
}
実行結果
shell:>【hoge】
Hello Spring Shell!!
shell:>【hello】
No command found for 'hello'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
-
@ShellMethod
のkey
で、コマンド名に任意の名前を指定できる
複数の名前を割り当てる
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod(value="Hello World", key={"hoge", "fuga"})
public void hello() {
System.out.println("Hello Spring Shell!!");
}
}
実行結果
shell:>【hoge】
Hello Spring Shell!!
shell:>【fuga】
Hello Spring Shell!!
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
Sample Commands
fuga, hoge: Hello World
-
key
には複数の名前を設定できるので、エイリアス的に複数の名前を1つのコマンドに割り当てることができる
引数の指定
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(int a, int b, int c) {
System.out.println("a=" + a + ", b=" + b + ", c=" + c);
}
}
実行結果
shell:>【hello 1 2 3】
a=1, b=2, c=3
shell:>【hello 1 2】
Parameter '--c int' should be specified
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【hello a 2 3】
Failed to convert from type [java.lang.String] to type [int] for value 'a'; nested exception is java.lang.NumberFormatException: For input string: "a"
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
- メソッドに引数を定義すると、コマンドに引数を渡せるようになる
- コマンドに渡した引数は、そのままの順序でメソッドの引数に渡される
- 引数が不足していたり、型変換できない値を渡した場合はエラーになる
名前を付けて引数を渡す
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(int a, int b, int fooBar) {
System.out.println("a=" + a + ", b=" + b + ", fooBar=" + fooBar);
}
}
実行結果
shell:>【hello --a 1 --b 2 --foo-bar 3】
a=1, b=2, fooBar=3
shell:>【hello --foo-bar 3 --a 1 --b 2】
a=1, b=2, fooBar=3
shell:>【hello --b 2 1 3】
a=1, b=2, fooBar=3
- 引数は名前を付けて指定することもできる
- 名前の指定は、
--【引数名】 【値】
と指定する - 引数名は、デフォルトではメソッドの引数名がそのまま使用される
- ただし、コマンド名と同じで二単語以上のキャメルケースの場合は、ハイフン区切りに置き換えられる(
--foo-bar
)
- ただし、コマンド名と同じで二単語以上のキャメルケースの場合は、ハイフン区切りに置き換えられる(
- 名前を付けない引数指定と、名前を付けた引数指定を混在させることができる
- その場合、名前を付けている引数がまず優先的にメソッド引数に割り当てられる
- そして、残ったメソッド引数に名前なしの引数が順番に割り当てられる
プレフィックスを変更する
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod(value="Hello World", prefix="-")
public void hello(int a) {
System.out.println("a=" + a);
}
}
実行結果
shell:>【hello -a 1】
a=1
-
@ShellMethod
のprefix
属性で、名前指定の際に引数名に付けるプレフィックス(--
)を変更できる
引数名を変更する
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(int a, @ShellOption("--foo") int b, @ShellOption({"-h", "--hoge"}) int c) {
System.out.println("a=" + a + ", b=" + b + ", c=" + c);
}
}
実行結果
shell:>【hello --a 1 --foo 2 -h 3】
a=1, b=2, c=3
shell:>【hello --a 1 --foo 2 --hoge 3】
a=1, b=2, c=3
- メソッド引数を
@ShellOption
でアノテートすると、value
属性でコマンド引数名を変更できる -
value
属性は配列指定ができるので、同じ引数に複数の名前を割り当てることができる
引数のデフォルト値
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(@ShellOption(defaultValue="9") int a) {
System.out.println("a=" + a);
}
}
実行結果
shell:>【hello】
a=9
shell:>【hello 1】
a=1
-
@ShellOption
のdefaultValue
属性で、その引数が指定されなかった場合のデフォルト値を定義できる
1つの引数で複数の値を受け取る
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import java.util.Arrays;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(@ShellOption(arity=3) int[] a, int b) {
System.out.println("a=" + Arrays.toString(a) + ", b=" + b);
}
}
実行結果
shell:>【hello 1 2 3 4】
a=[1, 2, 3], b=4
shell:>【hello --a 1 2 3 --b 4】
a=[1, 2, 3], b=4
shell:>【hello 1 --b 4 2 3】
a=[1, 2, 3], b=4
shell:>【hello --a 1 2 --b 4】
Failed to convert from type [java.lang.String] to type [@org.springframework.shell.standard.ShellOption int] for value '--b'; nested exception is java.lang.NumberFormatException: For input string: "--b"
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【hello --a 1 2 3 4 --b 5】
Too many arguments: the following could not be mapped to parameters: '4'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
- 1つのコマンド引数で複数の値を受け取るようにできる
- メソッド引数はコレクション型または配列型で定義する
- また、引数を
@ShellOption
でアノテートし、arity
属性で受け取る値の数を指定する
-
arity
で指定した数と異なる数の値を渡そうとするとエラーになる- 数を無制限にすることは現在できないっぽい(ドキュメントには
TO BE IMPLEMENTED
と書いてある)
- 数を無制限にすることは現在できないっぽい(ドキュメントには
boolean の引数
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(boolean a) {
System.out.println("a=" + a);
}
}
実行結果
shell:>【hello】
a=false
shell:>【hello --a】
a=true
- 引数の型が
boolean
の場合、コマンドでの指定方法が少し変わる - コマンド引数を何も指定しないと
false
になる - コマンド引数を指定する場合は、名前だけを指定して値は渡さない(
--a true
のように値を渡そうとするとエラーになる) - 引数を指定すると、それだけで
true
になる
boolean 引数にデフォルト値を指定した場合
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(@ShellOption(defaultValue="true") boolean a, @ShellOption(defaultValue="false") boolean b) {
System.out.println("a=" + a + ", b=" + b);
}
}
実行結果
shell:>【hello】
a=true, b=false
shell:>【hello --a --b】
a=false, b=true
- デフォルト値に
"false"
を設定した場合の動きは、何も設定しない場合と同じ- 引数を指定しないと
false
- 引数を指定すると
true
- 引数を指定しないと
- デフォルト値に
"true"
を設定すると、- 引数を指定しないと
true
に、 - 引数を指定すると
false
になる
- 引数を指定しないと
Bean Validation を利用する
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(@Min(0) @Max(100) int a) {
System.out.println("a=" + a);
}
}
実行結果
shell:>【hello -1】
The following constraints were not met:
--a int : must be greater than or equal to 0 (You passed '-1')
shell:>【hello 0】
a=0
shell:>【hello 100】
a=100
shell:>【hello 101】
The following constraints were not met:
--a int : must be less than or equal to 100 (You passed '101')
- Spring Shell は Bean Validation をサポートしており、メソッド引数に Bean Validation の制約アノテーションをつけることで入力チェックを実施できる
- 実装ライブラリは Hibernate Validator
- メッセージの日本語化とかは、こちらの方法 で実現できた
- ただし、 Bean Validation のエラーメッセージ以外は英語のまま
任意の型に変換する
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
@ShellMethod("Hello World")
public void hello(Hoge hoge) {
hoge.hello();
}
}
Hoge.java
package sample.spring.shell;
public class Hoge {
private final String value;
public Hoge(String value) {
this.value = value;
}
public void hello() {
System.out.println("Hoge(" + this.value + ")");
}
}
HogeConverter.java
package sample.spring.shell;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class HogeConverter implements Converter<String, Hoge> {
@Override
public Hoge convert(String source) {
return new Hoge(source);
}
}
実行結果
shell:>【hello Hey】
Hoge(Hey)
- コマンドメソッドの引数で基本型以外の任意の型を受け取りたい場合は、自作の
Converter
を定義することで実現できる -
org.springframework.core.convert.converter.Converter<S, T>
を実装したクラスを作成する-
T convert(S)
メソッドを実行して、S
型の値(多くはString
)を型T
に変換した結果を返す -
@Component
をつけてコンテナに登録されるようにする
-
コマンドの有効・無効を動的に切り替える
package sample.spring.shell;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
private boolean greeted;
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
this.greeted = true;
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
public Availability byeAvailability() {
return this.greeted
? Availability.available()
: Availability.unavailable("you does not greet yet.");
}
}
実行結果
shell:>【bye】
Command 'bye' exists but is not currently available because you does not greet yet.
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
Sample Commands
* bye: Good Bye
hello: Hello World
Commands marked with (*) are currently unavailable.
Type `help <command>` to learn more.
shell:>【hello】
Hello!!
shell:>【bye】
Bye!!
- コマンドによっては、特定の状態にならないと実行できないようにしたくなることがあるかもしれない
- 例えば、サーバーに接続して何かコマンドを発行するようなシェルを作ろうとしている場合、
接続するためのコマンドが成功するまでは、そのあとに使用するコマンドは使えないようにしておきたくなるかもしれない
- 例えば、サーバーに接続して何かコマンドを発行するようなシェルを作ろうとしている場合、
- Spring Shell では、コマンドの有効・無効を動的に切り替える仕組みが用意されている
- 無効なコマンドは、実行するとエラーになる
- 上記例では、
bye
コマンドはhello
コマンドを実行するまで無効になるようにしている -
bye
コマンドの有効・無効の判定は、byeAvailability()
メソッドで行っている- 有効・無効を動的に切り替えたいメソッドの名前に、
Availability
のサフィックスを付けたメソッドが、自動的に判定メソッドとして識別されるようになっている-
bye()
->byeAvailability()
-
- このメソッドは
Availability
というオブジェクトを返すように実装する -
Availability
クラスにはavailable()
とunavailable(String)
という2つのファクトリメソッドが用意されている - 有効な場合は、
available()
メソッドで生成したオブジェクトを返す - 無効な場合は、
unavailable()
メソッドで生成したオブジェクトを返す- このとき、引数には無効となっている理由を短い文章で渡しておく
- すると、エラーメッセージの
This command is currently not available because 【ここ】
に埋め込まれる - なので、小文字始まりでドット終わりになるように記述しておくと、良い感じになる
- 有効・無効を動的に切り替えたいメソッドの名前に、
- コマンドが有効か無効かの情報は、
help
を見たときの情報などに反映されるようになっている
有効・無効の判定メソッドの名前を任意のものにする
package sample.spring.shell;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellMethodAvailability;
@ShellComponent
public class SampleCommands {
private boolean greeted;
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
this.greeted = true;
}
@ShellMethod("Good Bye")
@ShellMethodAvailability("checkByeAvailability")
public void bye() {
System.out.println("Bye!!");
}
public Availability checkByeAvailability() {
return this.greeted
? Availability.available()
: Availability.unavailable("you does not greet yet.");
}
}
-
【対象のメソッド名】Availability
という命名規則が気に食わない場合は、任意の名前のメソッドに変更することもできる - 動的コマンドのメソッドに
@ShellMethodAvailability
アノテーションをつけて、value
属性に有効・無効を判定するメソッドの名前を指定する
複数のコマンドの有効・無効をまとめて制御する
package sample.spring.shell;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellMethodAvailability;
@ShellComponent
public class SampleCommands {
private boolean greeted;
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
this.greeted = true;
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
@ShellMethod("lol")
public void laugh() {
System.out.println("HAHAHAHA!!");
}
@ShellMethodAvailability({"bye", "laugh"})
public Availability checkAvailability() {
return this.greeted
? Availability.available()
: Availability.unavailable("you does not greet yet.");
}
}
実行結果
shell:>【laugh】
Command 'laugh' exists but is not currently available because you does not greet yet.
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【bye】
Command 'bye' exists but is not currently available because you does not greet yet.
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【hello】
Hello!!
shell:>【laugh】
HAHAHAHA!!
shell:>【bye】
Bye!!
- 複数のコマンドの有効・無効を同じ条件で制御したい場合、それぞれのメソッドを
@ShellMethodAvailability
でアノテートする必要はない - 代わりに、有効・無効の判定メソッドの方を
@ShellMethodAvailability
でアノテートする - そして、
value
属性に配列で対象となるコマンドの名前を指定する- コマンド名であり、メソッド名ではない点に注意
- つまり、メソッド名が
lotsOfLaugh
の場合、コマンド名はlots-of-laugh
になるので、@ShellMethodAvailability
に指定するのはlots-of-laugh
の方になる
クラス内の全てのコマンドの有効・無効をまとめて制御する
SampleCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class SampleCommands {
private boolean greeted;
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
this.greeted = true;
}
public boolean isGreeted() {
return this.greeted;
}
}
SomeCommands.java
package sample.spring.shell;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellMethodAvailability;
@ShellComponent
public class SomeCommands {
private final SampleCommands sampleCommands;
public SomeCommands(SampleCommands sampleCommands) {
this.sampleCommands = sampleCommands;
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
@ShellMethod("lol")
public void laugh() {
System.out.println("HAHAHAHA!!");
}
@ShellMethodAvailability
public Availability checkAvailability() {
return this.sampleCommands.isGreeted()
? Availability.available()
: Availability.unavailable("you does not greet yet.");
}
}
実行結果
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
Sample Commands
hello: Hello World
Some Commands
* bye: Good Bye
* laugh: lol
Commands marked with (*) are currently unavailable.
Type `help <command>` to learn more.
- クラス内の全コマンドをまとめて制御したい場合は、制御用のメソッドを
@ShellMethodAvailability
で注釈し、value
属性には何も設定しないようにする - これは、
value
属性のデフォルト値は*
という、全てのコマンドを対象にする特別なワイルドカードになっていることを利用している - これでクラス内の全てのコマンドが制御の対象になる
コマンドのグループ化
- コマンドの数が増えてくると、 help などを見やすくするためにもコマンドのグループ化をしたほうが良くなる
- Spring Shell では、コマンドを任意のグループにまとめる仕組みが用意されている
デフォルトのグループ化
GreetingCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class GreetingCommands {
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
}
CalcCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class CalcCommands {
@ShellMethod("a + b")
public int add(int a, int b) {
return a + b;
}
@ShellMethod("a - b")
public int minus(int a, int b) {
return a - b;
}
}
実行結果
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
Calc Commands
add: a + b
minus: a - b
Greeting Commands
bye: Good Bye
hello: Hello World
- 特にグループを指定しない場合、
@ShellComponent
で注釈したクラスごとにグループが定義され、その中で定義したコマンドはそのクラスに対応するグループに割り当てられる - グループ名は、クラス名を単語区切りにしたものになる
コマンドごとにグループを指定する
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class GreetingCommands {
@ShellMethod(value="Hello World", group="Hello")
public void hello() {
System.out.println("Hello!!");
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
}
実行結果
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
Greeting Commands
bye: Good Bye
Hello
hello: Hello World
-
@ShellMethod
のgroup
属性でグループ名を指定すると、コマンド単位でグループを指定できる
クラスごとにグループを指定する
GreetingCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellCommandGroup;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
@ShellCommandGroup("My Commands")
public class GreetingCommands {
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
}
CalcCommands.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellCommandGroup;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
@ShellCommandGroup("My Commands")
public class CalcCommands {
@ShellMethod("a + b")
public int add(int a, int b) {
return a + b;
}
@ShellMethod("a - b")
public int minus(int a, int b) {
return a - b;
}
}
実行結果
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
My Commands
add: a + b
bye: Good Bye
hello: Hello World
minus: a - b
- コマンドを定義したクラスを
@ShellCommandGroup
でアノテートする - そして、
value
属性でグループ名を指定すると、そのクラスの中で定義しているコマンドはそのグループに所属するようになる- 前述のコマンドごとの割り当てを指定している場合は、そちらが優先される
パッケージごとにグループを指定する
package-info.java
@ShellCommandGroup("my commands")
package sample.spring.shell;
import org.springframework.shell.standard.ShellCommandGroup;
実行結果
shell:>【help】
AVAILABLE COMMANDS
Built-In Commands
...
my commands
add: a + b
bye: Good Bye
hello: Hello World
minus: a - b
-
package-info.java
を作成し、パッケージを@ShellCommandGroup
でアノテートする - すると、そのパッケージ以下で定義したコマンドが、そこで指定したグループに所属するようになる
- 前述のクラス単位での割り当てがある場合は、そちらが優先される
コマンドのヘルプを見る
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class GreetingCommands {
@ShellMethod(value="Hello World")
public void hello(int a, @ShellOption(defaultValue="9", help="help text") int b) {
System.out.println("Hello!!");
}
}
実行結果
shell:>【help hello】
NAME
hello - Hello World
SYNOPSYS
hello [--a] int [[--b] int]
OPTIONS
--a int
[Mandatory]
--b int
help text
[Optional, default = 9]
-
help 【コマンド名】
とすると、指定したコマンドの詳細な説明を確認できる - 自作のコマンドも、定義情報をもとに良い感じの help を構築してくれるようになっている
プロンプトを変更する
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class GreetingCommands {
private boolean greeted;
@ShellMethod("Hello World")
public void hello() {
System.out.println("Hello!!");
this.greeted = true;
}
@ShellMethod("Good Bye")
public void bye() {
System.out.println("Bye!!");
}
public boolean isGreeted() {
return greeted;
}
}
MyPromptProvider.java
package sample.spring.shell;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.stereotype.Component;
@Component
public class MyPromptProvider implements PromptProvider {
private final GreetingCommands greetingCommands;
public MyPromptProvider(GreetingCommands greetingCommands) {
this.greetingCommands = greetingCommands;
}
@Override
public AttributedString getPrompt() {
return this.greetingCommands.isGreeted()
? new AttributedString("greeted > ", AttributedStyle.DEFAULT.foreground(AttributedStyle.WHITE))
: new AttributedString("not greeted > ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
}
実行結果
not greeted > 【hello】
Hello!!
greeted >
- プロンプトを変更するには、
PromptProvider
を実装したクラスを作成しコンテナに登録する -
getPrompt()
メソッドで、AttributedString
のインスタンスを返すように実装する -
AttributedString
は属性情報を持った(Attributed
)文字列で、文字のスタイル(太字や色など)を付加できる
組み込みコマンドのカスタマイズ
組み込みのコマンドを無効にする
build.gradle
dependencies {
compile('org.springframework.shell:spring-shell-starter:2.0.0.RELEASE') {
exclude module: 'spring-shell-standard-commands'
}
}
実行結果
shell:>【help】
No command found for 'help'
- 依存関係から
spring-shell-standard-commands
を除外すると、組み込みのコマンドを全て取り除くことができる -
exit
も消えるので、シェルを終了させるコマンドを自作しておかないと詰む(詰んだ)
特定のコマンドだけ無効にする
package sample.spring.shell;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.StringUtils;
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
String[] disabledCommands = {"--spring.shell.command.help.enabled=false"};
String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
SpringApplication.run(SampleApplication.class, fullArgs);
}
}
実行結果
shell:>【help】
No command found for 'help'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>【stacktrace】
org.springframework.shell.CommandNotFound: No command found for 'help'
at org.springframework.shell.Shell.evaluate(Shell.java:180)
at org.springframework.shell.Shell.run(Shell.java:134)
...
- 起動時の引数に
spring.shell.command.【コマンド名】.enabled=【true|false】
を指定することで、組み込みコマンドの有効・無効を制御できる - 上の例では起動時のコマンドライン引数で指定されたようなイメージだが、
application.properties
で指定することも可能(試してないけど、たぶん環境変数とかで指定できそう)
組み込みのコマンド動作を上書きする
SampleApplication.java
package sample.spring.shell;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.StringUtils;
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
String[] disabledCommands = {"--spring.shell.command.help.enabled=false"};
String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
SpringApplication.run(SampleApplication.class, fullArgs);
}
}
MyHelpCommand.java
package sample.spring.shell;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.commands.Help;
@ShellComponent
public class MyHelpCommand implements Help.Command {
@ShellMethod("My help command.")
public void help() {
System.out.println("HELP ME!!!!");
}
}
実行結果
shell:>【help】
HELP ME!!!!
- 組み込みのコマンドの動作を変更したい場合、次の手順で変更を加える
- 上書きしたい組み込みコマンドを無効にする
- 同じ名前で自作のコマンドを定義する
- このとき、自作コマンドの方のクラスは
【コマンド名】.Command
というインターフェースを実装するようドキュメントには書かれている- けど、別にこのインターフェースを実装しなくても動作はした(何に使われているのかはよく分からない)