Go の flag パッケージってとても便利ですよね。 CLI を作成するさいによくお世話になると思います。 しかし、bool のフラグに対して関数を渡せないのが不便です。今回はそれを解決する方法を紹介します。
※go1.17 時点の方法のため、go のバージョンアップで使用できなくなる可能性があります。
bool 値を渡せないとは?
flag パッケージには、flag.Funcというコールバック関数を受け取るものが用意されています。
func Func(name, usage string, fn func(string) error)
使い方は以下のような形です。
package main
import (
"flag"
"log"
)
func main() {
var arg string
flag.Func("hello", "set string", func(s string) error {
arg = s
return nil
})
flag.Parse()
log.Print(arg)
}
$ go run flag.go -hello ok
2022/01/04 22:30:16 ok
$ go run flag.go -hello ok -hello override
2022/01/04 22:30:23 override
このコールバック関数で渡されている string の s は見ての通り実行時にフラグに対して指定した値になります。
しかし、上記の関数は bool 値には使えません。なぜなら bool のフラグは引数を取らないからです。
func main() {
good := flag.Bool("good", false, "good")
flag.Parse()
log.Print(*good)
}
$ go run flag.go
2022/01/04 22:50:07 false
$ go run flag.go -good
2022/01/04 22:50:09 true
じゃあ渡せないの?ということではありません。
bool のフラグにコールバック関数を渡す方法
前提知識として、go におけるフラグの扱いを見ていきます。
go の flag パッケージの引数は様々な型に対応していますが、どの関数も基本的には flag.Varのラッパーです。
func Var(value Value, name string, usage string)
この flag.Valueを満たすように値をラップして渡すということをやっています。
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/flag/flag.go;l=793;drc=refs%2Ftags%2Fgo1.17.5
func Float64Var(p *float64, name string, value float64, usage string) {
CommandLine.Var(newFloat64Value(value, p), name, usage)
}
そうです。勿論 bool 値の時もラップしています。ではなぜ bool の時は挙動が変わるのでしょうか?
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/flag/flag.go;l=637;drc=refs%2Ftags%2Fgo1.17.5
unc BoolVar(p *bool, name string, value bool, usage string) {
CommandLine.Var(newBoolValue(value, p), name, usage)
}
その秘密はパース処理にあります。以下の処理はflag.Parseの内部処理です。
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/flag/flag.go;l=966-976;drc=refs%2Ftags%2Fgo1.17.5
if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
if hasValue {
if err := fv.Set(value); err != nil {
return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err)
}
} else {
if err := fv.Set("true"); err != nil {
return false, f.failf("invalid boolean flag %s: %v", name, err)
}
}
} else {
この処理で分かるように、boolFlag にキャスト可能な interface の時は bool 用の処理をすることがわかります。 そして、boolFlag の interface は以下です。
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/flag/flag.go;drc=refs%2Ftags%2Fgo1.17.5;l=133
type boolFlag interface {
Value
IsBoolFlag() bool
}
ここまで分かれば、コールバック関数を使える bool フラグを作ることなんて御茶の子さいさいです。
package main
import (
"flag"
"log"
)
type boolFunc func(string) error
func (boolFunc) String() string {
return "boolFunc"
}
func (f boolFunc) Set(s string) error {
return f(s)
}
func (boolFunc) IsBoolFlag() bool {
return true
}
func main() {
var reverse bool
flag.Var(boolFunc(func(_ string) error {
reverse = !reverse
return nil
}), "reverse", "")
flag.Parse()
log.Print(reverse)
}
$ go run flag.go
2022/01/04 23:15:17 false
$ go run flag.go -reverse
2022/01/04 23:15:20 true
$ go run flag.go -reverse -reverse
2022/01/04 23:15:23 false
注意
公開されていない interface のため、将来的に挙動が変更される可能性があります。 あくまで現在のバージョン(1.17)で使用できるハックとして捉えてもらえると助かります。