技術選択編 - #golang で CLI 作るときにいつもつかうやつ

続編 :point_right: [ボイラプレート編 - #golang で CLI 作るときにいつもつかうやつ - Qiita](https://qiita.com/izumin5210/items/25a27d4f3c5078588f79)

---

[grapi](https://github.com/izumin5210/grapi)・[gex](https://github.com/izumin5210/gex)・[ery](https://github.com/srvc/ery) など，今年に CLI を作りまくって見えてきたベストプラクティス集（技術選択編）．

## 基本便利パッケージ
### Cobra - [spf13/cobra](https://github.com/spf13/cobra)
https://github.com/spf13/cobra

フラグ処理やサブコマンド・ヘルプメッセージ・補完など，CLI ツールに必要なことはだいたいいい感じにやってくれるライブラリ．有名どころだと docker や kubectl, [hugo](https://github.com/gohugoio/hugo) の実装に利用されている．

使い方イメージ:


```go
// cmd/foobar/main.go
//----------------------------------------------------------------

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() {
	cmd := cmd.NewFoobarCommand()
	return cmd.Execute()
}


// pkg/foobar/cmd/cmd.go
//----------------------------------------------------------------

func NewFoobarCommand() *cobra.Command {
	var (
		flagVerbose bool
	)

	cmd := &cobra.Command{
		Use: "foobar",
	}

	cmd.AddCommand(
		newInitCommand(),
	)

	// `Persistent` ってつくやつはサブコマンドまで影響が及ぶやつ
	cmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "enable verbose log")

	return cmd
}

// pkg/foobar/cmd/init.go
//----------------------------------------------------------------

func newInitCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use: "init",
		Args: cobra.ExactArgs(1), // 引数のバリデーションもできる
		RunE: func(_ *cobra.Command, args []string) error {
			// snip.
			return nil
 		},
	}

	return cmd
}
```

[`Command` インスタンスにいろいろ設定して](https://godoc.org/github.com/spf13/cobra#Command) `Execute()` するだけの親切設計．
内部は [pflag](https://github.com/spf13/pflag) や後述する [viper](https://github.com/spf13/viper) などをくみあわせてできており，多機能に見えてライブラリ自体は薄い．


### Viper - [spf13/viper](https://github.com/spf13/viper)

yaml, toml などの設定ファイル・環境変数・フラグなどの設定値を透過的に扱えるようにするライブラリ．とくに言うこともなく，ただただ便利．

```go
v := viper.New()

// 環境変数
v.EnvPrefix("app")
v.BindEnv("host") // APP_HOST

// 設定ファイル
v.SetConfigName(".app") // .app.toml, .app.yaml, ...
v.AddConfigPath(".")    // ./.app.toml, ./.app.yaml, ...
err = v.ReadInConfig()  // ファイル読み込み
if err != nil {
	// ...
}

// flag
viper.BindPFlag("author", cmd.Flags().Lookup("author"))

// 環境変数・設定ファイル・flag からいい感じに値を取り出す
v.Get("author")

// struct に unmarshal もできる
type Config struct {
	Author string
}
err = v.Unmarshal(&cfg)
```

## testability を上げるためのパッケージ
### Afero - [spf13/afero](https://github.com/spf13/afero)

A FileSystem Abstraction System という description がすべてを語っている通り，ファイルシステムの抽象化ライブラリ．要するに「`os` パッケージのファイル系関数や `io/ioutil` の代わりに使うやつ」．

ファイルシステムのバックエンドを指定できるようになっていて，普通は OS のファイルシステム `afero.OsFs` を利用すればよい．ただ，「ユニットテストを書くときにいちいち OS のファイルシステムに書き出すのは大変なので，テスト時はモック `afero.MemMapFs` に差し替える」みたいなことができる．

```go
func GenerateFile(fs afero.Fs, params interface{}) error {
	buf := new(bytes.Buffer)
	err := tmpl.Execute(buf, params)
	if err != nil {
		// ...
	}

	err = afero.WriteFile(fs, "awesome.go", buf.Bytes(), 0644)
	if err != nil {
		// ...
	}

	return nil
}

// ふつうにファイルを生成
fs := afero.NewOsFs()
GenerateFile(fs, params)

// テスト用のモック filesystem にファイルを生成
fs := afero.NewMemMapFs()
GenerateFile(fs, params)
```

自分がよくやるケースとして，「ファイル生成周りのユニットテストは`afero.MemMapFs` に吐かせたものを [**cupaloy**](https://github.com/bradleyjkemp/cupaloy) に通して snapshot testing をする」というものがある．


### [k8s.io/utils/exec](https://godoc.org/k8s.io/utils/exec) - [kubernetes/utils](https://github.com/kubernetes/utils)

`exec.Command` を使うコードをテストできるようにするライブラリ．`exec.Command` している箇所を `k8s.io/utils/exec.Interface` 経由に差し替えて使う．

```go
exec := exec.New()

cmd := exec.Command("echo", "Hello world!")
buf := new(bytes.Buffer)
cmd.SetStdout(buf)
err := cmd.Run()
if err != nil {
	// ...
}
fmt.Println(buf.String()) // Hello world!
```

外部パッケージを使わずとも，`exec.Command` 周りのテストは様々な手法がある:

- [Go で外部コマンドをユニットテストする - Qiita](https://qiita.com/yuya_takeyama/items/11cb60466a3f11dd1e3b)
- [GoのテストでOSコマンド実行だけをモックする（変態編） - Qiita](https://qiita.com/hnw/items/d1a89267c2da7e4317ee)

自分はだいたい `exec.Command` をラップした，外部コマンド実行用の interface を自前で用意することが多かった．
`k8s.io/utils/exec` も同様に interface にして差し替え可能にするという点では同じだが，便利ポイントとして[テスト用の fake 実装](https://godoc.org/k8s.io/utils/exec/testing) も用意されている点で優秀．

ちなみに `k8s.io/utils` は kubernetes から切り出された「kubernetes には直接関係のない，標準パッケージに関係する便利パッケージ群」．


## どこまでやるか
自分の場合，「作るものが単機能であることが確定している場合」には上述のものを使わないことがある．[gex](https://github.com/izumin5210/gex) がその例．

- フラグハンドリングだけ `pflag` を使う
- testability はあまり考えない
- テストを書くことによる心理的安全を確保したい場合はとりあえず E2E テストで頑張る
    - （ビルドしたバイナリを Docker コンテナ内で実行する）

ただ，だんだんコードベースが大きくなってきて E2E だけだとつらい（テストカバレッジを上げたい）場合や機能が増えてくる場合は前述のパッケージ群を徐々に投入していく場合もある．