chzyer/readline で作る cowshell

最近 go の [github trends](https://github.com/trending/go?since=weekly) で [gitql](https://github.com/cloudson/gitql) という sql 文を使ってローカルリポジトリの情報を確認することができるというツールが急上昇していました. そこで issue の中にインタラクティブなモードを実装するというのがあったので, これは PR チャンスだと思い今回インタラクティブモードを作成しました. そのインタラクティブモードを実装する上で使用したパッケージが [github.com/chzyer/readline](https://github.com/chzyer/readline) でした. 今回それが素晴らしかったので, みなさんにも是非使ってもらいたく紹介します！

# 使い方
サンプルコードが用意されていてこういう感じで使えばいいのかと分かりやすいです.
[https://github.com/chzyer/readline/tree/master/example](https://github.com/chzyer/readline/tree/master/example)
しかし初挑戦という方は難しいかもしれないので, もう少し噛み砕いたサンプルを作ってみました.

```go:cowshell.go
package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	cowsay "github.com/Code-Hex/Neo-cowsay"
	"github.com/chzyer/readline"
)

func usage(w io.Writer) {
	io.WriteString(w, "commands:\n")
	io.WriteString(w, completer.Tree("    "))
}

// 補完機能の作成
var completer = readline.NewPrefixCompleter(
	readline.PcItem("help"),
	// » cow [tab とした時の補完候補
	readline.PcItem("cow",
		readline.PcItem("say",
			readline.PcItem("rainbow"),
			readline.PcItem("aurora"),
		),
		readline.PcItem("think"),
	),
)

func main() {
	// readline の初期化
	l, err := readline.NewEx(&readline.Config{
		Prompt:          "\033[31m»\033[0m ",
		HistoryFile:     "./cowsh.hist",
		AutoComplete:    completer,
		InterruptPrompt: "^C",
		EOFPrompt:       "exit",
	})
	if err != nil {
		panic(err)
	}
	defer l.Close()

	for {
		line, err := l.Readline()
		// ^D もしくは ^C でループを抜ける
		if err == io.EOF || err == readline.ErrInterrupt {
			break
		}

		// 両端の ' ', '\n', '\t', '\r' を削除
		// https://golang.org/pkg/strings/#TrimSpace
		line = strings.TrimSpace(line)

		cmds := strings.Split(line, " ")
		ln := len(cmds)

		if ln > 2 {
			mow := &cowsay.Cow{
				Type:        "default",
				BallonWidth: 40,
			}

			if strings.Contains(cmds[1], "think") {
				mow.Thinking = true
				mow.Phrase = cmds[2]
			} else {
				if ln > 3 {
					if strings.Contains(cmds[2], "rainbow") {
						mow.IsRainbow = true
					} else if strings.Contains(cmds[2], "aurora") {
						mow.IsAurora = true
					}
					mow.Phrase = cmds[3]
				} else {
					mow.Phrase = cmds[2]
				}
			}

			say, err := cowsay.Say(mow)
			if err != nil {
				fmt.Println(err)
			}
			fmt.Println(say)
		} else {
			usage(os.Stderr)
		}

	}
}
```

ちょこっと説明します.

```go
readline.NewEx(&readline.Config{
        Prompt:          "\033[31m»\033[0m ",
        HistoryFile:     "./cowsh.hist",
        AutoComplete:    completer,
        InterruptPrompt: "^C",
        EOFPrompt:       "exit",
    })
```
を読んでみましょう.
- `Prompt` enter キーを入力する度に一番左に現れる文字列を指定することができます.
- `HistoryFile` 今まで実行したコマンドをファイルに書き出して保存することができます. 今回はカレントディレクトリに作成されるようになっています.
- `InterruptPrompt` ctrl+c でシェルから抜け出した時に出力する文字列を指定できます.
- `EOFPrompt` ctrl+d でシェルから抜け出した時に出力する文字列を指定できます.

ここで少し難しいのは補完機能です. 今回はこのように簡単に作りました.

```
var completer = readline.NewPrefixCompleter(
    readline.PcItem("help"),
    // » cow [tab とした時の補完候補
    readline.PcItem("cow",
        readline.PcItem("say",
            readline.PcItem("rainbow"),
            readline.PcItem("aurora"),
        ),
        readline.PcItem("think"),
    ),
)
```
例えば

```
readline.PcItem("cow",
        readline.PcItem("say",
            readline.PcItem("rainbow"),
            readline.PcItem("aurora"),
        ),
        readline.PcItem("think"),
    )
```
の場合だと `cow` を入力した後 tab キーを入力すると `say`, `think` が候補に現れ, `cow say` を入力した後 tab キーを入力すると `rainbow`, `aurora` を候補として出力することができます.
# 実行
実行するとこんな感じになります.
![スクリーンショット 2016-12-24 23.29.58.png](https://qiita-image-store.s3.amazonaws.com/0/118068/5af94c84-e74e-8c77-37a5-f358f8cd689b.png)

一番下は tab キーを入力して補完を行っている様子です.
さらにこの cowshell を実行しているカレントディレクトリには cowsh.hist というファイルも作成されているはずです.

皆さんも是非試してみてください！