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を利用する機能が存在している。ただ、これを使ったとして短いオプションに対応するための差分を考えると、きれいなヘルプメッセージと少ない差分のどちらかを取ることになるんじゃないかなーとは思っている。

gist