goのflagパッケージは-xxxだけでなく--xxxにも対応していることを知った
-name
というオプションを持つようなコマンドを作ってみるとヘルプメッセージは以下の様になる。
$ go run main.go -h Usage of app: -name string name of person (default "foo")
素直にヘルプメッセージを読むと使えるのは、-name <value>
と -name=<value>
だけ。
実際には --name
にも対応しているということを知ったのでメモ。
テスト
-name
を取るようなFlagSetを作る。Output部分はテストのためのコード。
parse.go
package parse import ( "flag" "strings" ) type Option struct { Name string // required } func New(opt *Option) *flag.FlagSet { cmd := flag.NewFlagSet("app", flag.ContinueOnError) cmd.StringVar(&opt.Name, "name", "", "<name>") var b strings.Builder cmd.SetOutput(&b) return cmd }
テストコードを書いてみて以下を調べる。
- Parse -- 引数が渡されたときの扱い
- ParseDefault -- 引数が渡されなかった場合の扱い
- Help -- ヘルプメッセージの扱い
parse_test.go
package parse import ( "flag" "fmt" "strings" "testing" ) func TestParse(t *testing.T) { cases := [][]string{ {"-name", "foo"}, {"--name", "foo"}, {"-name=foo"}, {"--name=foo"}, } for _, args := range cases { args := args t.Run(fmt.Sprintf("%q", args), func(t *testing.T) { opt := &Option{} cmd := New(opt) err := cmd.Parse(args) if err != nil { t.Fatalf("expect nil, but error %+v", err) } if opt.Name != "foo" { t.Errorf("expected %q, but %q", "foo", opt.Name) } }) } } func TestParseDefault(t *testing.T) { args := []string{} opt := &Option{} cmd := New(opt) err := cmd.Parse(args) if err != nil { t.Fatalf("expect nil, but error %+v", err) } if opt.Name != "" { t.Errorf("expected %q, but %q", "", opt.Name) } } func TestHelp(t *testing.T) { cases := [][]string{ {"-h"}, {"--help"}, } for _, args := range cases { args := args t.Run(fmt.Sprintf("%q", args), func(t *testing.T) { opt := &Option{} cmd := New(opt) err := cmd.Parse(args) if err == nil { t.Fatal("expect error, but nil") } if err != flag.ErrHelp { t.Errorf("must be %s", err) } output := cmd.Output().(*strings.Builder).String() expected := `Usage of app: -name string <name> ` if expected != output { t.Errorf( "expected %q, but %q", strings.TrimSpace(output), strings.TrimSpace(expected), ) } }) } }
テスト結果
たしかに --name=xxx
にも --name xxx
にも対応している。
$ go test -v === RUN TestParse === RUN TestParse/["-name"_"foo"] === RUN TestParse/["--name"_"foo"] === RUN TestParse/["-name=foo"] === RUN TestParse/["--name=foo"] --- PASS: TestParse (0.00s) --- PASS: TestParse/["-name"_"foo"] (0.00s) --- PASS: TestParse/["--name"_"foo"] (0.00s) --- PASS: TestParse/["-name=foo"] (0.00s) --- PASS: TestParse/["--name=foo"] (0.00s) === RUN TestParseDefault --- PASS: TestParseDefault (0.00s) === RUN TestHelp === RUN TestHelp/["-h"] === RUN TestHelp/["--help"] --- PASS: TestHelp (0.00s) --- PASS: TestHelp/["-h"] (0.00s) --- PASS: TestHelp/["--help"] (0.00s) PASS ok m/01parse 0.007s
実はflagパッケージだけで良いのでは?
今までなんとなくでコマンドラインオプションのライブラリを使っていたが、flagパッケージだけで良いかもしれないと思い始めてきた。
実際に使うとしたら
実際に使うとしたら、main.goは以下の様な感じにすると思う。Optionの構造体は作らないかもしれない。
main.go
package main import ( "flag" "fmt" "log" "os" ) type Option struct { Name string } func main() { opt := &Option{} cmd := flag.NewFlagSet("app", flag.ContinueOnError) cmd.StringVar(&opt.Name, "name", "foo", "name of person") cmd.StringVar(&opt.Name, "n", "foo", "(shorthand of --name)") if err := cmd.Parse(os.Args[1:]); err != nil { if err != flag.ErrHelp { cmd.Usage() } os.Exit(1) } if err := run(opt); err != nil { log.Fatalf("!!%+v", err) } } func run(opt *Option) error { Hello(opt.Name) return nil } func Hello(name string) { fmt.Printf("Hello %s\n", name) }
個人的なgoに対するイメージとして機能が満たされていれば見てくれはあまり気にしない、それよりも実装の単純さを取るというような認識がある。
なのでまぁ不格好ではあるが -n
のような短いオプション(short option)に対応するときには以下のような表示になってもまぁいいかと思ったりしている。
$ go run main.go -h Usage of app: -n string (shorthand of --name) (default "foo") -name string name of person (default "foo")
--name
に対応できてるし。-h
と--help
にも対応しているし。
実行結果
$ go run main.go -n xxx Hello xxx $ go run main.go --name yyy Hello yyy $ go run main.go --name=zzz Hello zzz
これから
かつて書いたこの記事をflagパッケージだけを使う形で書き換えてみても良いかもしれない。ただし心境の変化もあってkingpinではなくpflagあたりを使いそう。
後々気にしたくなるのは以下あたり?
- サブコマンドの作成
- required/unrequiredの対応
- (サブコマンドをimportしやすいパッケージ構成)
ちなみに
(ちなみにkingpinは開発が止まり、kingpinの作者はkongというライブラリを公開している)
ちなみにpflagを利用してmain.goを書くと以下の様なdiffになる。
--- 00hello/main.go 2020-04-22 02:39:31.000000000 +0900 +++ 02hello/main.go 2020-04-22 02:50:00.000000000 +0900 @@ -1,10 +1,11 @@ package main import ( - "flag" "fmt" "log" "os" + + flag "github.com/spf13/pflag" ) type Option struct { @@ -15,8 +16,7 @@ opt := &Option{} cmd := flag.NewFlagSet("app", flag.ContinueOnError) - cmd.StringVar(&opt.Name, "name", "foo", "name of person") - cmd.StringVar(&opt.Name, "n", "foo", "(shorthand of --name)") + cmd.StringVarP(&opt.Name, "name", "n", "foo", "name of person") if err := cmd.Parse(os.Args[1:]); err != nil { if err != flag.ErrHelp {
あと、pflagはflagパッケージのFlagSetを利用する機能が存在している。ただ、これを使ったとして短いオプションに対応するための差分を考えると、きれいなヘルプメッセージと少ない差分のどちらかを取ることになるんじゃないかなーとは思っている。