cobra-cmder で Go の CLI を簡単に作る

## 概要

Go 言語における CLI アプリ作成で一番使われる定番のライブラリといえば [spf13/cobra](https://github.com/spf13/cobra) でしょう。 Hugo や Kubernetes といった著名なプロジェクトでも cobra を使っているそうです。

しかしながら自分の場合、フラグの値を格納するのにグローバル変数を使ったり、初期化で `init()` を使ったりという cobra の流儀が気に入らず、これまで利用を敬遠してきました。一番問題だと考えているのはユニットテストの記述が困難であることです。

もちろん cobra でもグローバル変数や `init()` を一切使わず構造体のフィールドやメソッドだけで構成することは可能ですが、その場合 [cobra コマンド (コードジェネレータ)](https://github.com/spf13/cobra/blob/master/cobra/README.md) の支援はなく、すべて手で書く必要があります。

先日より、上記の問題点を解消しようと取り組んだところ、簡単かつエレガントな cobra の構成手法を確立することができました。そのためのライブラリを [yaegashi/cobra-cmder](https://github.com/yaegashi/cobra-cmder) というモジュールとして公開したので紹介します。

## 基本的な使用法

cobra-cmder で CLI を作る例題として、次のような呼び出し方ができる app というアプリを考えてみます。

```console
$ app alpha one -h
Usage:
  app alpha one [flags]

Flags:
  -h, --help      help for one
  -i, --int int   Int flag

Global Flags:
  -b, --bool            Bool flag
  -s, --string string   String flag

$ app alpha one -b -s abc -i 123
true abc 123
```

フラグ (オプション) は次のように各コマンドで定義されています。また、上位コマンドのフラグは下位コマンドでも使うことができるものとします。

|フラグ|型|コマンド|
|---|---|---|
|`-b` `--bool`|`bool`|`app`|
|`-s` `--string`|`string`|`app alpha`|
|`-i` `--int`|`int`|`app alpha one`|


### コマンド型の定義

まず、各コマンドの専用型となる構造体を定義します。各コマンド型には、フラグの値を格納するフィールドと、上位コマンド型を参照する埋め込みフィールド (最上位のコマンド型を除く) を用意しておきます。

```go
// App - app command
type App struct {
	Bool bool // storage for flag -b
}

// AppAlpha - app alpha command
type AppAlpha struct {
	*App          // storage for parent Cmder (embedded)
	String string // storage for flag -s
}

// AppAlphaOne - app alpha one command
type AppAlphaOne struct {
	*AppAlpha     // storage for parent Cmder (embedded)
	Int       int // storage for flag -i
}
```

コマンド型の名前はなんでもかまいませんが、この例のようにコマンド階層を反映したものにしておくことをおすすめします。

### cmder.Cmder インターフェースの実装

cobra-cmder で重要な役割を演じるのは、次に示す `cmder.Cmder` インターフェースです。

```go
type Cmder interface {
	Cmd() *cobra.Command
}
```

次の手順では、各コマンド型について `cobra.Command` を返す `Cmd()` メソッドを実装することで、 この `cmder.Cmder` インターフェースに適合するようにしていきます。

```go
func (app *App) Cmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "app",
	}
	cmd.PersistentFlags().BoolVarP(&app.Bool, "bool", "b", false, "Bool flag")
	return cmd
}

func (app *AppAlpha) Cmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "alpha",
	}
	cmd.PersistentFlags().StringVarP(&app.String, "string", "s", "", "String flag")
	return cmd
}

func (app *AppAlphaOne) Cmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "one",
		Run: app.Run,
	}
	cmd.Flags().IntVarP(&app.Int, "int", "i", 0, "Int flag")
	return cmd
}

func (app *AppAlphaOne) Run(cmd *cobra.Command, args []string) {
	fmt.Println(app.Bool, app.String, app.Int)
}
```

最後の `AppAlphaOne` 型のメソッドについて、次の点に注目してください。

- `AppAlphaOne.Cmd()` で特定インスタンスに対するメソッド呼び出し `app.Run` を関数として設定しています。このような呼び出しは Go 言語では [method values](https://golang.org/ref/spec#Method_values) としてサポートされています。
- `AppAlphaOne.Run()` では `app.Bool` `app.String` のように上位のコマンド型で定義されたフィールドに直接アクセスしています。これができるのは各コマンド型の定義で上位コマンド型の埋め込みをしているからです。

上位コマンドを含むフラグの値を格納した変数へのアクセスが、グローバル変数を使うことなく自然な形で実現できています。

### コマンド階層の関連付け

次のようなメソッド定義により、各コマンドの階層の関連付けを行います。

```go
func (app *App) AppAlphaCmder() cmder.Cmder         { return &AppAlpha{App: app} }
func (app *AppAlpha) AppAlphaOneCmder() cmder.Cmder { return &AppAlphaOne{AppAlpha: app} }
```
上位コマンド型に下位コマンド型のインスタンスを生成し `cmder.Cmder` として返すメソッドを追加します。また下位コマンド型インスタンスには上位コマンド型インスタンスのポインタを設定します。

これらのメソッドの名前はなんでも構いません。ただし `cmder.Cmder` の全階層にわたりユニークな必要があるので、この例のようにコマンド型の名前を一部に使用することをおすすめします。

### cobra.Command の生成と実行

最後に `main()` を実装します。次のように `cmder.Cmd()` 関数に最上位コマンド型のインスタンスを渡して呼び出すことにより、コマンド型の階層をトラバースしてすべての設定が反映された `cobra.Command` を生成することができます。これはそのまま `Execute()` メソッドで実行できます。

```go
func main() {
	app := &App{}
	cmd := cmder.Cmd(app)
	err := cmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}
```

以上で CLI の実装は完了です。完全なソースコードが [Go Playground](https://play.golang.org/p/zw4arxJfUkt) にあるので参照してください。 `main()` の `cmd.SetArgs()` により、様々なコマンドライン引数を与えて動作確認ができます。

### ユニットテスト

cobra-cmder を使えばグローバル変数や `init()` を使わずに cobra を構成できますので、CLI のユニットテストが容易にできます。 [Go Playgound](https://play.golang.org/p/tijvjDzmwqW) に例があるので参照してください。

CLI からの出力をモッキングするため、最上位コマンド型 `App` に対して変数 `Out` とメソッド `Print` `Println` `Printf` を追加しています。

```go
type App struct {
	Out  io.Writer // mocking output
	Bool bool      // storage for flag -b
}

func (app *App) Print(args ...interface{}) (int, error) {
	return fmt.Fprint(app.Out, args...)
}

func (app *App) Println(args ...interface{}) (int, error) {
	return fmt.Fprintln(app.Out, args...)
}

func (app *App) Printf(format string, args ...interface{}) (int, error) {
	return fmt.Fprintf(app.Out, format, args...)
}
```

これらは `AppAlphaOne` のような下位コマンド型からも呼び出して使うことができます。

```go
func (app *AppAlphaOne) Run(cmd *cobra.Command, args []string) {
	app.Println(app.Bool, app.String, app.Int)
}
```

テスト本体では次のように Go 言語で標準的なテーブルベースのテストが実現できています。

```go
func TestApp(t *testing.T) {
	tests := []struct {
		args []string
		want string
		err  bool
	}{
		{args: []string{"alpha", "one", "-b", "-s", "abc", "-i", "123"}, want: "true abc 123\n", err: false},
		{args: []string{}, want: "", err: false},
		{args: []string{"alpha"}, want: "", err: false},
		{args: []string{"alpha", "one"}, want: "false  0\n", err: false},
		{args: []string{"beta"}, want: "", err: false},
		{args: []string{"alpha", "-i", "123"}, want: "false  123", err: false},
	}
	for _, tt := range tests {
		args := strings.Join(tt.args, " ")
		buf := &bytes.Buffer{}
		cmd := cmder.Cmd(&App{Out: buf})
		cmd.SetArgs(tt.args)
		cmd.SetOut(ioutil.Discard)
		cmd.SetErr(ioutil.Discard)
		err := cmd.Execute()
		if tt.err && err == nil {
			t.Errorf("%q returns no error", args)
		}
		if !tt.err && err != nil {
			t.Errorf("%q returns error: %s", args, err)
		}
		got := string(buf.Bytes())
		if got != tt.want {
			t.Errorf("%q returns %q, want %q", args, got, tt.want)
		}
	}
}
```

このテストの実行結果は次のとおりです。

```
=== RUN   TestApp
    TestApp: prog.go:103: "beta" returns error: unknown command "beta" for "app"
    TestApp: prog.go:103: "alpha -i 123" returns error: unknown shorthand flag: 'i' in -i
    TestApp: prog.go:107: "alpha -i 123" returns "", want "false  123"
--- FAIL: TestApp (0.00s)
FAIL
```

## その他の使用例

サンプルコード [cmd/sample](https://github.com/yaegashi/cobra-cmder/tree/master/cmd/sample) は、 cobra-cmder を使ったより複雑なコマンド階層の構築例です。

このサンプルでは、ひとつのコマンドごとにひとつのファイルを使用しています。メソッド定義の追加という非侵襲的な手法でコマンド階層の関連付けしていることから、このように各コマンドの実装をファイル単位で独立させることができ、コマンドの追加・削除などのメンテナンスが容易になっています。

cobra-cmder は次のような CLI アプリケーションでも使われています。

- https://github.com/yaegashi/contest.go
- https://github.com/yaegashi/azbill

## cobra-cmder の実装

cobra-cmder は `cmder.Cmder` インターフェースと `cmder.Cmd()` という小さな関数で構成されます。これらは [cmder.go ファイル](https://github.com/yaegashi/cobra-cmder/blob/master/cmder.go)に実装されています。

```go
// Cmd traverses a Cmder hierarchy and returns a configured cobra.Command.
// It recursively calls all methods that return a Cmder
// to collect and associate cobra.Command instances.
func Cmd(c Cmder) *cobra.Command {
	return recCmd(c, map[string]bool{})
}

// outTypes is a constant for the array of function output types
var outTypes = []reflect.Type{reflect.TypeOf((*Cmder)(nil)).Elem()}

// recCmd is the actual worker function to visit and collect Cmder instances.
// mmap is for bookkeeping already visted method names.
func recCmd(c Cmder, mmap map[string]bool) *cobra.Command {
	cmd := c.Cmd()
	inV := reflect.ValueOf(c)
	inT := reflect.TypeOf(c)
	funcT := reflect.FuncOf([]reflect.Type{inT}, outTypes, false)
	methods := []reflect.Method{}
	for i := 0; i < inT.NumMethod(); i++ {
		m := inT.Method(i)
		if m.Func.Type() != funcT || mmap[m.Name] {
			continue
		}
		methods = append(methods, m)
	}
	for _, m := range methods {
		mmap[m.Name] = true
	}
	for _, m := range methods {
		subC := m.Func.Call([]reflect.Value{inV})[0].Interface().(Cmder)
		subCmd := recCmd(subC, mmap)
		cmd.AddCommand(subCmd)
	}
	for _, m := range methods {
		mmap[m.Name] = false
	}
	return cmd
}
```

関数の本体である `cmder.recCmd()` はリフレクションを利用し、引数のコマンド型インスタンスに定義されたメソッドで `cmder.Cmder` を返すものを呼び出し、得られた `cmder.Cmder` を下位のコマンド型として、自分自身を再帰的に呼び出します。

そうして集めた下位コマンドの `cobra.Command` は、引数のコマンド型インスタンスの `Cmd()` メソッドで得た `cobra.Command` に `AddCommand()` メソッドで関連付けられます。これを戻り値として関数を終了します。

上位コマンド型の埋め込みをしている場合、上位コマンド型で定義されたメソッドもリフレクションで取得できてしまいますが、それらは呼び出しのループを避けるために呼ばないようにする必要があります。

しかしながら Go 言語のランタイムでは、埋め込みのフィールドに由来するメソッドとそうでないメソッドを区別する方法がありません。そこでやむなく `mmap` マップにより一度呼んだメソッド名を記録する手段を採用しています。コマンド型の階層間でメソッド名がユニークな必要があるという制約はこの実装により生じています。

## まとめ

cobra-cmder を使えば cobra でグローバル変数や `init()` を使うことなく CLI のコマンド階層の構築ができることを示しました。この手法には次のような特長があります。

- 各コマンドの実装の記述が簡単
- グローバル変数および `init()` の排除によりユニットテストの記述が容易
- method values や埋め込みフィールドの活用による、コマンド階層を反映したわかりやすいストレージアクセス
- メソッド定義を用いた非侵襲的なコマンド階層の関連付け手法による、各コマンドの追加・削除などのメンテナンス性の向上

その一方で次の改善点が考えられます。

- `cmder.Cmd()` ではリフレクションを利用しており実行の都度コマンド型の階層をトラバースしているため、実行開始までに時間がかかる。ビルド前に静的なコマンド階層構築のコードを自動生成できることが望ましい。

以上です。気に入ったらぜひ使ってみてください。プルリクエストもお待ちしております！