Go flagパッケージを読んで自分なりに理解してみた
この記事はWano Group Advent Calendar2021 15日目の記事です。
初めに
パッケージリーディング
Goの標準パッケージは基本的にGoで記述されていて、Goの思想と相まって比較的読みやすいと思います。
特にGoの思想やイディオムなど標準パッケージを読むことでより理解できると思うので、今回はflagパッケージを読んでみます。
読んでみて初めて知る使い方などもあったりするのでおすすめです。
今回はflagパッケージのexampleから辿るとflagパッケージのほとんどの使い方をさらえそうだったので、そこから内部の実装を辿っていこうと思います。
バージョン1.17.5時点のものです。
flag Package
Package flag implements command-line flag parsing.
flagパッケージはコマンドラインflagのパースができるパッケージです。
後々コードを読むとわかりますが、flagでは os.Args
で取得できる引数の中でも、 -
, --
で始まるものがパース対象になっています。
オプション
と呼ぶことも多いと思いますが、ほぼ同義かと思います。
今回flagパッケージを選んだ理由としては以下の感じです。
- 1000ステップくらいしかない
- その中でも各型にinterfaceを満たすための同じような実装をしていたりする部分も多く、メインの処理がシンプル
- 他のパッケージを使うような処理もシンプルなものが多く、大体パッケージ内で完結する
-
flag.Parse()
って定型文みたいになってるけど内部で何をしているんだろう?くらいの理解で、インプットの余地があった
Example
まずはUsageにあるexampleを見てみましょう。コメント部分は概ね削除しています。
// These examples demonstrate more intricate uses of the flag package.
このexampleはflagパッケージの使い方の中でも複雑な方らしいです。
// Example 1: A single string flag called "species" with default value "gopher".
// Example 2: Two flags sharing a variable, so we can have a shorthand.
// The order of initialization is undefined, so make sure both use the
// same default value. They must be set up with an init function.// Example 3: A user-defined flag type, a slice of durations.
type interval []time.Duration
具体的には3つの例が提示されていて、以下のような感じそうです。
-
-species
を使ってstringのflagをパースする。さらに"gopher"
がデフォルト値になるようにする -
2つのフラグ
gopher_type
,g
が変数を共有することで、shorthandを実現する -
ユーザー定義のフラグタイプ
type interval []time.Duration
に対してパースする
package main
import (
"errors"
"flag"
"fmt"
"strings"
"time"
)
var species = flag.String("species", "gopher", "the species we are studying")
var gopherType string
func init() {
const (
defaultGopher = "pocket"
usage = "the variety of gopher"
)
flag.StringVar(&gopherType, "gopher_type", defaultGopher, usage)
flag.StringVar(&gopherType, "g", defaultGopher, usage+" (shorthand)")
}
type interval []time.Duration
func (i *interval) String() string {
return fmt.Sprint(*i)
}
func (i *interval) Set(value string) error {
if len(*i) > 0 {
return errors.New("interval flag already set")
}
for _, dt := range strings.Split(value, ",") {
duration, err := time.ParseDuration(dt)
if err != nil {
return err
}
*i = append(*i, duration)
}
return nil
}
var intervalFlag interval
func init() {
flag.Var(&intervalFlag, "deltaT", "comma-separated list of intervals to use between events")
}
func main() {
// All the interesting pieces are with the variables declared above, but
// to enable the flag package to see the flags defined there, one must
// execute, typically at the start of main (not init!):
// flag.Parse()
// We don't run it here because this is not a main function and
// the testing suite has already parsed the flags.
}
実行してみる
どんな使い方ができるのか実際に実行してみましょう。
exampleのmain関数を以下のように書き換え、パース結果の変数を表示してみます。
func main() {
flag.Parse()
fmt.Print("*species = ")
fmt.Println(species)
fmt.Print("gopherType = ")
fmt.Println(gopherType)
fmt.Print("intervalFlag = ")
fmt.Println(intervalFlag)
}
実行スクリプトを用意します
go build -o flag_ex flag_ex.go &&\
echo "$ ./flag_ex" && ./flag_ex &&\
echo "$ ./flag_ex -species=supergopher" && ./flag_ex -species=supergopher &&\
echo "$ ./flag_ex -gopher_type=Hedgehog" && ./flag_ex -gopher_type=Hedgehog &&\
echo "$ ./flag_ex -g=Hedgehog" && ./flag_ex -g=Hedgehog &&\
echo "$ ./flag_ex -deltaT=300ms,1h45m" && ./flag_ex -deltaT=300ms,1h45m
↓実行結果
$ sh flag_ex.sh | (while read line; do echo " $line"; done)
$ ./flag_ex
*species = gopher
gopherType = pocket
intervalFlag = []
$ ./flag_ex -species=supergopher
*species = supergopher
gopherType = pocket
intervalFlag = []
$ ./flag_ex -gopher_type=Hedgehog
*species = gopher
gopherType = Hedgehog
intervalFlag = []
$ ./flag_ex -g=Hedgehog
*species = gopher
gopherType = Hedgehog
intervalFlag = []
$ ./flag_ex -deltaT=300ms,1h45m
*species = gopher
gopherType = pocket
intervalFlag = [300ms 1h45m0s]
それぞれ以下のフラグ指定によって変数が更新されています。
0. フラグを何も指定しない場合、デフォルト値が入る
- species:
-species=supergopher
- gopherType:
-gopher_type=Hedgehog
or-g=Hedgehog
- intervalFlag:
-deltaT=300ms,1h45m
3の intervalFlag
は出力からはわかりづらいですが、フラグのパースのタイミングで各要素を string -> time.Duration
へ変換しています。
前提
まずはパッケージ読み込み時に実行される部分
// A FlagSet represents a set of defined flags. The zero value of a FlagSet
// has no name and has ContinueOnError error handling.
//
// Flag names must be unique within a FlagSet. An attempt to define a flag whose
// name is already in use will cause a panic.
type FlagSet struct {
// Usage is the function called when an error occurs while parsing flags.
// The field is a function (not a method) that may be changed to point to
// a custom error handler. What happens after Usage is called depends
// on the ErrorHandling setting; for the command line, this defaults
// to ExitOnError, which exits the program after calling Usage.
Usage func()
name string
parsed bool
actual map[string]*Flag
formal map[string]*Flag
args []string // arguments after flags
errorHandling ErrorHandling
output io.Writer // nil means stderr; use Output() accessor
}
...
// CommandLine is the default set of command-line flags, parsed from os.Args.
// The top-level functions such as BoolVar, Arg, and so on are wrappers for the
// methods of CommandLine.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
...
// NewFlagSet returns a new, empty flag set with the specified name and
// error handling property. If the name is not empty, it will be printed
// in the default usage message and in error messages.
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
f := &FlagSet{
name: name,
errorHandling: errorHandling,
}
f.Usage = f.defaultUsage
return f
}
パッケージ変数 CommandLine
の初期化をしています。
os.Args[0]
にはプログラム名が入るので、 NewFlagSet()
にはプログラム名と、パースエラーが起きた際にどういったハンドリングをするのかが引数として与えられます。
NewFlagSet()
内部では上記2つと、デフォルトのusageを使って初期化した FlagSet
を返しています。
この後の処理ではFlagSetにどのようにフラグをパースするのかといった情報などを追加していきます。
Example1
まずはプログラム起動時の処理を追います。
var species = flag.String("species", "gopher", "the species we are studying")
Example1はシンプルで、 フラグ名: "species"
, デフォルト値: "gopher"
, usage: "the species we are studying"
を用いて、 flag.String()
を呼び出しているようです。
flag.String()
を追っていきます。
func (f *FlagSet) StringVar(p *string, name string, value string, usage string) {
f.Var(newStringValue(value, p), name, usage)
}
...
func (f *FlagSet) String(name string, value string, usage string) *string {
p := new(string)
f.StringVar(p, name, value, usage)
return p
}
...
func String(name string, value string, usage string) *string {
return CommandLine.String(name, value, usage)
}
String() -> CommandLine.String() -> (*FlagSet).StringVar()
の順で呼び出しており、 引数 name(フラグ名), value(デフォルト値), usage
を、 パース結果の格納先 p *string
の宣言とともに f.Var(newStringValue(value, p), name, usage)
の呼び出しを行なっているようです。
以下に列挙する他の型もほとんど同じ構成になっていて、これがステップ数にして20%くらいを占めます。
具体的には以下の型をサポートしているので、これらを使うか、後述する flag.Value
インターフェースを満たす独自型を定義して使用することになります。
ジェネリクスが入った後はこの辺りはかなりまとまる可能性はありそうですが、後方互換性を保つためにジェネリクスを使用して定義された関数を呼び出すだけのような実装になったりするんですかね?
flag.Bool()
flag.Int()
flag.Uint()
flag.String()
flag.Float64()
flag.Duration() // time.Duration
flag.Func() // func(string) error
まずは newStringValue()
type Value interface {
String() string
Set(string) error
}
...
// -- string Value
type stringValue string
func newStringValue(val string, p *string) *stringValue {
*p = val
return (*stringValue)(p)
}
func (s *stringValue) Set(val string) error {
*s = stringValue(val)
return nil
}
func (s *stringValue) Get() interface{} { return string(*s) }
func (s *stringValue) String() string { return string(*s) }
格納先変数にデフォルト値をセットした後、 *stringValue
に変換しています。
stringValue
には先述の Value
インターフェースを満たすメソッドが定義されています。
String() string
はフラグのゼロ値が返されることが期待され、
Set(string) error
は各フラグに対してパース時に1度だけ呼ばれ、引数として受け取った文字列を変数にどのように格納するかが記述されることが期待されます。
続いて (*FlagSet).Var()
// A Flag represents the state of a flag.
type Flag struct {
Name string // name as it appears on command line
Usage string // help message
Value Value // value as set
DefValue string // default value (as text); for usage message
}
...
func (f *FlagSet) Var(value Value, name string, usage string) {
// Remember the default value as a string; it won't change.
flag := &Flag{name, usage, value, value.String()}
_, alreadythere := f.formal[name]
if alreadythere {
var msg string
if f.name == "" {
msg = fmt.Sprintf("flag redefined: %s", name)
} else {
msg = fmt.Sprintf("%s flag redefined: %s", f.name, name)
}
fmt.Fprintln(f.Output(), msg)
panic(msg) // Happens only if flags are declared with identical names
}
if f.formal == nil {
f.formal = make(map[string]*Flag)
}
f.formal[name] = flag
}
これまでの情報から、 Flag
構造体を作り、 FlagSet.formal
にフラグ名をキーとして追加しています。
FlagSet.formal
は、 map[string]*Flag
となっており、キーはフラグ名になっているようです。
FlagSet.Var()
の処理を見てみると、フラグ名が重複した場合、上書きではなくパニックが起きることがわかります。これは起動タイミングで実行される想定なので、エラーを返す形ではなくpanicを起こしているものと思われます。
ここまででフラグを受け取る準備が完了しました。
これが基本となり、他のExampleも同じような流れをたどります。
次は実際のパース処理について追っていきます。
func main() {
flag.Parse()
}
利用者側これだけです。
内部では、ここまでで準備してきた変数 CommandLine *FlagSet
と、 os.Args
を利用して、実際のパース処理を行います。
func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}
...
func (f *FlagSet) Parse(arguments []string) error {
f.parsed = true
f.args = arguments
for {
seen, err := f.parseOne()
if seen {
continue
}
if err == nil {
break
}
switch f.errorHandling {
case ContinueOnError:
return err
case ExitOnError:
if err == ErrHelp {
os.Exit(0)
}
os.Exit(2)
case PanicOnError:
panic(err)
}
}
return nil
}
flag.Parse()
では、 CommandLine.Parse(os.Args[1:])
を呼び出しています。
os.Args[0]
にはプログラム名が入っているので、それ以降のコマンドライン引数を使用しています。
(*FlagSet).Parse()
では、 渡されてきたコマンドライン引数を args
に格納し、 f.parseOne()
を、返り値 seen
がfalseになるまで、呼び続けています。
また、 CommandLine.Parse(os.Args[1:])
がエラーを無視しているのは、 CommandLine.errorHandling
には ExitOnError
がセットされており、パースエラーが起きた際には os.Exit()
が呼び出されるためと思われます。
独自で定義し、 errorHandling
に ContinueOnError
をセットしている場合は、 (*FlagSet).Parse()
のエラーは呼び出し側でハンドリングするべきでしょう。(どちらかというと呼び出し側でエラーをハンドリングするためにContinueOnErrorをセットするはず)
実際の各フラグのパース処理 parseOne()
に入っていきます。
若干長いので、分割してみてみます。
func (f *FlagSet) parseOne() (bool, error) {
if len(f.args) == 0 {
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {
return false, nil
}
numMinuses := 1
if s[1] == '-' {
numMinuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
name := s[numMinuses:]
if len(name) == 0 || name[0] == '-' || name[0] == '=' {
return false, f.failf("bad flag syntax: %s", s)
}
// it's a flag. does it have an argument?
f.args = f.args[1:]
...
}
f.args[0]
を取得し、 -
or --
で始まっていることを確認後、 name
にハイフン以降を格納し、 f.args
から f.args[0]
を取り除きます。
f.args = []string{"-species=supergopher"}
↓
f.args = []string{}
name = "species=supergopher"
func (f *FlagSet) parseOne() (bool, error) {
...
// it's a flag. does it have an argument?
f.args = f.args[1:]
hasValue := false
value := ""
for i := 1; i < len(name); i++ { // equals cannot be first
if name[i] == '=' {
value = name[i+1:]
hasValue = true
name = name[0:i]
break
}
}
m := f.formal
flag, alreadythere := m[name] // BUG
if !alreadythere {
if name == "help" || name == "h" { // special case for nice help message.
f.usage()
return false, ErrHelp
}
return false, f.failf("flag provided but not defined: -%s", name)
}
...
}
name
に =
が含まれる場合、 =
以前・以降をそれぞれ name
, value
に格納します。
その後、 f.formal
に name
をキーにした Flag
がセットされているかをチェックし、セットされていない場合にヘルプの場合を除きエラーを返しています。
name = "species=supergopher"
↓
name = "species"
value = "supergopher"
func (f *FlagSet) parseOne() (bool, error) {
...
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 {
...
}
ここが実際に value
を flag.Value
にセットする処理です。
その中でまずは flag.ValueがboolFlagインターフェースを満たし、IsBoolFlag() == trueのケース
をみてみます。
boolFlag
インターフェースは何かというと、フラグをboolで受け取る場合は、値を省略するのが一般的だと思うので、そういった値を必要としないもののために定義されているインターフェースですね。
$ command -b=TRUE // case1
$ command -b // case2
// case1
name = "b"
value = "TRUE"
hasValue = true
↓
fv.Set("TRUE")
// case2
name = "b"
hasValue = false
↓
fv.Set("true")
func (f *FlagSet) parseOne() (bool, error) {
...
} else {
// It must have a value, which might be the next argument.
if !hasValue && len(f.args) > 0 {
// value is the next arg
hasValue = true
value, f.args = f.args[0], f.args[1:]
}
if !hasValue {
return false, f.failf("flag needs an argument: -%s", name)
}
if err := flag.Value.Set(value); err != nil {
return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
}
}
if f.actual == nil {
f.actual = make(map[string]*Flag)
}
f.actual[name] = flag
return true, nil
}
ここは flag.ValueがboolFlagインターフェースを満たし、IsBoolFlag() == trueのケース
以外の処理と、 Set()
が成功した場合に f.actual
に flag
をセットし、 seen == true
を返す最後の処理です。
はじめの処理をみると、 !hasValue && len(f.args) > 0
のケースに、次の引数を value
としています。
ここで、以下が同じ結果になることがわかります。これも一般的だと思うので、イメージしやすいと思います。
$ ./flag_ex -species supergopher // case1
$ ./flag_ex -species=supergopher // case2
// case1
name = "species"
value = ""
hasValue = false
f.args = []string{"supergopher"}
↓
name = "species"
value = "supergopher"
hasValue = true
f.args = []string{}
↓
fv.Set("supergopher")
// case2
name = "species"
value = "supergopher"
hasValue = true
f.args = []string{}
↓
fv.Set("supergopher")
これでやっと事前準備したフラグセットに実際に渡されたフラグをパースしながらセットすることができました。
var species = flag.String("species", "gopher", "the species we are studying") // *species = "supergopher"
Example2
すみません。疲れました。
ですが、幸いここまでで2・3の中身も理解できるので、軽くだけ触ります。
var gopherType string
func init() {
const (
defaultGopher = "pocket"
usage = "the variety of gopher"
)
flag.StringVar(&gopherType, "gopher_type", defaultGopher, usage)
flag.StringVar(&gopherType, "g", defaultGopher, usage+" (shorthand)")
}
func StringVar(p *string, name string, value string, usage string) {
CommandLine.Var(newStringValue(value, p), name, usage)
}
StringVar()
は CommandLine.Var(newStringValue(value, p), name, usage)
を呼んでいるだけなので、それ以降はExample1と同じ。
$ ./flag_ex -gopher_type=Hedgehog
*species = gopher
gopherType = Hedgehog
intervalFlag = []
$ ./flag_ex -g=Hedgehog
*species = gopher
gopherType = Hedgehog
intervalFlag = []
Example3
type interval []time.Duration
func (i *interval) String() string {
return fmt.Sprint(*i)
}
func (i *interval) Set(value string) error {
if len(*i) > 0 {
return errors.New("interval flag already set")
}
for _, dt := range strings.Split(value, ",") {
duration, err := time.ParseDuration(dt)
if err != nil {
return err
}
*i = append(*i, duration)
}
return nil
}
var intervalFlag interval
func init() {
flag.Var(&intervalFlag, "deltaT", "comma-separated list of intervals to use between events")
}
独自型 interval
に flag.Value
インターフェースを満たすメソッドを定義して、特に Set(value string) error
によって、 value
に自由にユーザー定義のロジックで値をセットすることができるようになっています。
$ ./flag_ex -deltaT=300ms,1h45m
*species = gopher
gopherType = pocket
intervalFlag = [300ms 1h45m0s]
終わりに
個人的に今回で勉強になったと感じるところ
- フラグの使用方法
- シンプルな
flag.Value
インターフェースを中心にすることで、 コード量を共通化しつつ利用者側に柔軟な操作を提供している -
flag.String()
,flag.StringVar()
などの薄いラッパーを用意することで、利用者側が細部を意識せず利用できる - 現在の実装を理解しておくことで、ジェネリクスが入った際にどういった修正をするのか参考にできそう
- 各所の簡潔かつ理解の助けになるコメント
- gopher_type=pocket の意味
Discussion