Spring Shell 使い方メモ

# Spring Shell とは
- REPL ツールを簡単に作れるようにするためのフレームワーク
- Web アプリのようなリッチな UI が必要ない場合で、インタラクティブな CUI のみで十分な場合などに利用できる
- ベースには JLine が使用されている（jshell でも利用されているライブラリ）

# 環境
## OS
Windows 10

## Java
1.8.0_162

# Hello World
## 実装
```groovy: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'
}
```

```java: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);
	}
}
```

```java: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`  |最後に発生したエラーのスタックトレースを表示する|


```text: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` |１つ前の単語までを削除 |
|`Ctrl` + `d` |カーソル位置の文字を削除 |
|`Ctrl` + `f` |カーソルを１つ進める  |
|`Ctrl` + `b` |カーソルを１つ戻す    |
|`Alt` + `f` |カーソルを１単語進める |
|`Alt` + `b` |カーソルを１単語戻す |

## 空白スペースを含む値を渡す
```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(String text) {
        System.out.println(text);
    }
}
```

```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'
```

- 空白スペースを含む文字列を引数に渡したい場合は、シングルクォーテーション (`'`) またはダブルクォーテーション (`"`) で文字列を括る
- シングルクォーテーションの中ではダブルクォーテーションがそのまま利用でき、ダブルクォーテーションの中ではシングルクォーテーションがそのまま利用できる
- シングルクォーテーションの中でシングルクォーテーションを、ダブルクォーテーションの中でダブルクォーテーションを使用したい場合は、バックスラッシュ(`\`)でエスケープする

```text:空白スペースをエスケープする
shell:>【hello foo\ bar】
foo bar
```

- クォーテーションで括らなくても、空白スペース自体をエスケープする方法もある

### 複数行の入力
```text:実行結果
shell:>【hello "abc】
dquote> 【defg】
dquote> 【hijk"】
abc defg hijk
```

- クォーテーションを開始した状態で改行を入れると、引き続き文字の入力が促されるようになる
- クォーテーションを閉じるまで、改行を含めた入力が１つの入力として処理される（改行自体は、最終的に空白スペースに置き換えられる）



## Tab による入力補完
![springshell.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/28ef5050-2c47-004e-5dc2-2d76bc5595c7.gif)

- `Tab` を入力すると様々な場所で入力補完が働くようになっている
    - 候補が表示されると、引き続き `Tab` を入力することで順番に選択肢にカーソルが移動する
    - `Enter` で候補を選択できる
- コマンドの補完だけでなく、引数の補完も対応している

## １つのコマンド入力に改行を含める
```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(int a, int b, int c) {
        System.out.println("a=" + a + ", b=" + b + ", c=" + c);
    }
}
```

```text:実行結果
shell:>【hello \】
> 【--a 10 \】
> 【--b 20 \】
> 【--c 30】
a=10, b=20, c=30
```

- バックスラッシュ (`\`) で区切ることで、１つのコマンドの入力を複数行に分けて記述できる


# コマンドの定義
```java: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!!");
    }
}
```

```text:実行結果
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` で、コマンド名に任意の名前を指定できる

### 複数の名前を割り当てる
```java: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!!");
    }
}
```

```text:実行結果
shell:>【hoge】
Hello Spring Shell!!

shell:>【fuga】
Hello Spring Shell!!

shell:>【help】
AVAILABLE COMMANDS

Built-In Commands
        ...

Sample Commands
        fuga, hoge: Hello World
```

- `key` には複数の名前を設定できるので、エイリアス的に複数の名前を１つのコマンドに割り当てることができる

# 引数の指定
```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(int a, int b, int c) {
        System.out.println("a=" + a + ", b=" + b + ", c=" + c);
    }
}
```

```text:実行結果
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.
```

- メソッドに引数を定義すると、コマンドに引数を渡せるようになる
- コマンドに渡した引数は、そのままの順序でメソッドの引数に渡される
- 引数が不足していたり、型変換できない値を渡した場合はエラーになる

## 名前を付けて引数を渡す
```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(int a, int b, int fooBar) {
        System.out.println("a=" + a + ", b=" + b + ", fooBar=" + fooBar);
    }
}
```

```text:実行結果
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`)
- 名前を付けない引数指定と、名前を付けた引数指定を混在させることができる
    - その場合、名前を付けている引数がまず優先的にメソッド引数に割り当てられる
    - そして、残ったメソッド引数に名前なしの引数が順番に割り当てられる

## プレフィックスを変更する
```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", prefix="-")
    public void hello(int a) {
        System.out.println("a=" + a);
    }
}
```

```text:実行結果
shell:>【hello -a 1】
a=1
```

- `@ShellMethod` の `prefix` 属性で、名前指定の際に引数名に付けるプレフィックス（`--`）を変更できる

## 引数名を変更する
```java
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);
    }
}
```

```text:実行結果
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` 属性は配列指定ができるので、同じ引数に複数の名前を割り当てることができる

## 引数のデフォルト値
```java
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);
    }
}
```

```text:実行結果
shell:>【hello】
a=9

shell:>【hello 1】
a=1
```

- `@ShellOption` の `defaultValue` 属性で、その引数が指定されなかった場合のデフォルト値を定義できる

## １つの引数で複数の値を受け取る
```java
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);
    }
}
```

```text:実行結果
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.
```

- １つのコマンド引数で複数の値を受け取るようにできる
    - メソッド引数はコレクション型または配列型で定義する
    - また、引数を `@ShellOption` でアノテートし、 `arity` 属性で受け取る値の数を指定する
- `arity` で指定した数と異なる数の値を渡そうとするとエラーになる
    - 数を無制限にすることは現在できないっぽい（ドキュメントには `TO BE IMPLEMENTED` と書いてある）

## boolean の引数
```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(boolean a) {
        System.out.println("a=" + a);
    }
}
```

```text:実行結果
shell:>【hello】
a=false

shell:>【hello --a】
a=true
```

- 引数の型が `boolean` の場合、コマンドでの指定方法が少し変わる
- コマンド引数を何も指定しないと `false` になる
- コマンド引数を指定する場合は、名前だけを指定して値は渡さない（`--a true` のように値を渡そうとするとエラーになる）
- 引数を指定すると、それだけで `true` になる

### boolean 引数にデフォルト値を指定した場合
```java
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);
    }
}
```

```text:実行結果
shell:>【hello】
a=true, b=false

shell:>【hello --a --b】
a=false, b=true
```

- デフォルト値に `"false"` を設定した場合の動きは、何も設定しない場合と同じ
    - 引数を指定しないと `false`
    - 引数を指定すると `true`
- デフォルト値に `"true"` を設定すると、
    - 引数を指定しないと `true` に、
    - 引数を指定すると `false` になる

## Bean Validation を利用する
```java
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);
    }
}
```

```text:実行結果
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
- メッセージの日本語化とかは、[こちらの方法](https://qiita.com/opengl-8080/items/3926fbde5469c0b330c2#%E3%83%93%E3%83%AB%E3%83%88%E3%82%A4%E3%83%B3%E3%81%AE%E5%88%B6%E7%B4%84%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%99%E3%82%8B) で実現できた
    - ただし、 Bean Validation のエラーメッセージ以外は英語のまま

## 任意の型に変換する
```java: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();
    }
}
```

```java: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 + ")");
    }
}
```

```java: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);
    }
}
```

```text:実行結果
shell:>【hello Hey】
Hoge(Hey)
```

- コマンドメソッドの引数で基本型以外の任意の型を受け取りたい場合は、自作の `Converter` を定義することで実現できる
- `org.springframework.core.convert.converter.Converter<S, T>` を実装したクラスを作成する
    - `T convert(S)` メソッドを実行して、 `S` 型の値（多くは `String`）を型 `T` に変換した結果を返す
    - `@Component` をつけてコンテナに登録されるようにする

# コマンドの有効・無効を動的に切り替える
```java
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.");
    }
}
```

```text:実行結果
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)` という２つのファクトリメソッドが用意されている
    - 有効な場合は、 `available()` メソッドで生成したオブジェクトを返す
    - 無効な場合は、 `unavailable()` メソッドで生成したオブジェクトを返す
        - このとき、引数には無効となっている理由を短い文章で渡しておく
        - すると、エラーメッセージの `This command is currently not available because 【ここ】` に埋め込まれる
        - なので、小文字始まりでドット終わりになるように記述しておくと、良い感じになる
- コマンドが有効か無効かの情報は、 `help` を見たときの情報などに反映されるようになっている

## 有効・無効の判定メソッドの名前を任意のものにする
```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 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` 属性に有効・無効を判定するメソッドの名前を指定する

## 複数のコマンドの有効・無効をまとめて制御する
```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 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.");
    }
}
```

```text:実行結果
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` の方になる

## クラス内の全てのコマンドの有効・無効をまとめて制御する
```java: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;
    }
}
```

```java: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.");
    }
}
```

```text:実行結果
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 では、コマンドを任意のグループにまとめる仕組みが用意されている

## デフォルトのグループ化
```java: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!!");
    }
}
```

```java: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;
    }
}
```

```text:実行結果
shell:>【help】
AVAILABLE COMMANDS

Built-In Commands
        ...

Calc Commands
        add: a + b
        minus: a - b

Greeting Commands
        bye: Good Bye
        hello: Hello World
```

- 特にグループを指定しない場合、 `@ShellComponent` で注釈したクラスごとにグループが定義され、その中で定義したコマンドはそのクラスに対応するグループに割り当てられる
- グループ名は、クラス名を単語区切りにしたものになる

## コマンドごとにグループを指定する
```java
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!!");
    }
}
```

```text:実行結果
shell:>【help】
AVAILABLE COMMANDS

Built-In Commands
        ...

Greeting Commands
        bye: Good Bye

Hello
        hello: Hello World
```

- `@ShellMethod` の `group` 属性でグループ名を指定すると、コマンド単位でグループを指定できる

## クラスごとにグループを指定する
```java: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!!");
    }
}
```

```java: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;
    }
}
```

```text:実行結果
shell:>【help】
AVAILABLE COMMANDS

Built-In Commands
        ...

My Commands
        add: a + b
        bye: Good Bye
        hello: Hello World
        minus: a - b
```

- コマンドを定義したクラスを `@ShellCommandGroup` でアノテートする
- そして、 `value` 属性でグループ名を指定すると、そのクラスの中で定義しているコマンドはそのグループに所属するようになる
    - 前述のコマンドごとの割り当てを指定している場合は、そちらが優先される

## パッケージごとにグループを指定する
```java:package-info.java
@ShellCommandGroup("my commands")
package sample.spring.shell;

import org.springframework.shell.standard.ShellCommandGroup;
```

```text:実行結果
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` でアノテートする
- すると、そのパッケージ以下で定義したコマンドが、そこで指定したグループに所属するようになる
- 前述のクラス単位での割り当てがある場合は、そちらが優先される

# コマンドのヘルプを見る
```java
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!!");
    }
}
```

```text:実行結果
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 を構築してくれるようになっている

# プロンプトを変更する
```java
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;
    }
}
```

```java: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));
    }
}
```

```text:実行結果
not greeted > 【hello】
Hello!!

greeted >
```

- プロンプトを変更するには、 `PromptProvider` を実装したクラスを作成しコンテナに登録する
- `getPrompt()` メソッドで、`AttributedString` のインスタンスを返すように実装する
- `AttributedString` は属性情報を持った(`Attributed`)文字列で、文字のスタイル（太字や色など）を付加できる

# 組み込みコマンドのカスタマイズ
## 組み込みのコマンドを無効にする
```groovy:build.gradle
dependencies {
	compile('org.springframework.shell:spring-shell-starter:2.0.0.RELEASE') {
		exclude module: 'spring-shell-standard-commands'
	}
}
```

```text:実行結果
shell:>【help】
No command found for 'help'
```

- 依存関係から `spring-shell-standard-commands` を除外すると、組み込みのコマンドを全て取り除くことができる
- `exit` も消えるので、シェルを終了させるコマンドを自作しておかないと詰む（詰んだ）

## 特定のコマンドだけ無効にする
```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);
	}
}
```

```text:実行結果
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` で指定することも可能（試してないけど、たぶん環境変数とかで指定できそう）

## 組み込みのコマンド動作を上書きする
```java: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);
	}
}
```

```java: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!!!!");
    }
}
```

```text:実行結果
shell:>【help】
HELP ME!!!!
```

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



# 参考
- [Spring Shell Reference Documentation](https://docs.spring.io/spring-shell/docs/2.0.1.BUILD-SNAPSHOT/reference/htmlsingle/)