Edited at
Go4Day 5

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

続編 :point_right: ボイラプレート編 - #golang で CLI 作るときにいつもつかうやつ - Qiita


grapigexery など,今年に CLI を作りまくって見えてきたベストプラクティス集(技術選択編).


基本便利パッケージ


Cobra - spf13/cobra

https://github.com/spf13/cobra

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

使い方イメージ:

// 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 インスタンスにいろいろ設定して Execute() するだけの親切設計.

内部は pflag や後述する viper などをくみあわせてできており,多機能に見えてライブラリ自体は薄い.


Viper - spf13/viper

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

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

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

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

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 に通して snapshot testing をする」というものがある.


k8s.io/utils/exec - kubernetes/utils

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

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 周りのテストは様々な手法がある:

自分はだいたい exec.Command をラップした,外部コマンド実行用の interface を自前で用意することが多かった.

k8s.io/utils/exec も同様に interface にして差し替え可能にするという点では同じだが,便利ポイントとしてテスト用の fake 実装 も用意されている点で優秀.

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


どこまでやるか

自分の場合,「作るものが単機能であることが確定している場合」には上述のものを使わないことがある.gex がその例.


  • フラグハンドリングだけ pflag を使う

  • testability はあまり考えない

  • テストを書くことによる心理的安全を確保したい場合はとりあえず E2E テストで頑張る


    • (ビルドしたバイナリを Docker コンテナ内で実行する)



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