角待ちは対空

おもむろガウェイン

alecthomas/kingpin でテスタビリティの高いCLIツールを作る

flagでは素朴すぎるし、巷で言及されてそうなコマンドライン引数オプションのパースライブラリはちょっとしっくりこなかったので、なんか良いのないですかと聞いたら alecthomas/kingpin っていうのを教えてもらったので使ってみた。

github.com

感触としては良さそうだったが、テストしやすく書こうと思ったら少しハマったので解決策をメモ。ドキュメント見れば解決するレベルの話。

io.Writer を渡せるように

//https://github.com/t-mrt/gocha/blob/bd8de43ad9293c712025cf79c9e10ca82346b7c0/cmd/gocha/main.go#L5-L8
func main() {
    cli := &CLI{outStream: os.Stdout, errStream: os.Stderr}
    os.Exit(cli.Run(os.Args))
}

テストしやすくするため実処理( この場合 cli.Run )と main() は分離し、その際に io.Writer が交換可能にしておく、というテクニックは一般的らしい。詳しくは Go言語でテストしやすいコマンドラインツールをつくる | SOTA を参照。

kingpin はデフォルトでは os.Stdout 及び os.Stderr が i/o になっているので書換える必要がある。

// https://github.com/t-mrt/gocha/blob/bd8de43ad9293c712025cf79c9e10ca82346b7c0/cmd/gocha/cli.go#L28-L30
var app = kingpin.New("gocha", "Random strings generater based on a pattern.")

var pattern = app.Arg("pattern", "Regular expression").Required().String()
var num = app.Flag("number-of-lines", "Number of lines").Short('n').Int()

var exitCode int
kingpin.CommandLine.Writer(c.errStream).Terminate(func(i int) {
    exitCode = i
})
kingpin.MustParse(app.Parse(args[1:]))

if exitCode == ExitCodeError {
    return ExitCodeError
}

特に説明することはないが kingpin.CommandLine.Writer(c.errStream) のように指定すればよい。

os.Exit(status) を呼ばないように

cli.Run() はステータスを返り値にしたいわけだけれども、 kingpin はパースに失敗すると( 必須の引数がないなど )直接 os.Exit() を呼ぶというのがデフォルトの動作となっている。まぁパース部分はライブラリの責任なのでテストしなくてもいいといえばいいが、 cli.Run() 内で直接 os.Exit() されてしまうのは少し気持ちが悪い。

幸いこれにもちゃんと対処法が用意されていて Terminate() でコールバック関数を指定できる。残念ながら返り値は返せないので、 exitCode を書き換え、パースした後if文でエラーだった場合 ExitCodeError を返す形になっている。

宣伝

gocha という正規表現にもとづいてランダムに文字列を出力するライブラリ/CLIツールをつくった。

github.com

% gocha -n 5 '[カコヵか][ッー]{1,3}?[フヒふひ]{1,3}[ィェー]{1,3}[ズス][ドクグュ][リイ][プブぷぶ]{1,3}[トドォ]{1,2}'
カーーーヒェィスクリぷぶブトト
ヵッーふひーズドイぶォト
ヵッふフィズグリぷド
ヵッフフェェィズクイぶトド
カッッッひひェーズグリぶトト

String_random.js の活用方法 - 氾濫原 の 「コーフィースクリップトの発音を生成する」より。