Java
spring
SpringShell

Spring Shell 使い方メモ

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 による入力補完

springshell.gif

  • 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 でアノテートする
    • @ShellMethodvalue にはコマンドを説明する文言を設定する必要がある(設定しておかないと、起動時にエラーになる)
    • 説明の文言は、他のコマンドとの整合性を取るため次の条件を満たすように記述するのが良い
      1. 短い文章(1,2文程度)
      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.
  • @ShellMethodkey で、コマンド名に任意の名前を指定できる

複数の名前を割り当てる

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
  • @ShellMethodprefix 属性で、名前指定の際に引数名に付けるプレフィックス(--)を変更できる

引数名を変更する

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
  • @ShellOptiondefaultValue 属性で、その引数が指定されなかった場合のデフォルト値を定義できる

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
  • @ShellMethodgroup 属性でグループ名を指定すると、コマンド単位でグループを指定できる

クラスごとにグループを指定する

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!!!!
  • 組み込みのコマンドの動作を変更したい場合、次の手順で変更を加える
    1. 上書きしたい組み込みコマンドを無効にする
    2. 同じ名前で自作のコマンドを定義する
  • このとき、自作コマンドの方のクラスは 【コマンド名】.Command というインターフェースを実装するようドキュメントには書かれている
    • けど、別にこのインターフェースを実装しなくても動作はした(何に使われているのかはよく分からない)

参考