Go flagパッケージを読んで自分なりに理解してみた

18 min read

この記事は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つの例が提示されていて、以下のような感じそうです。

  1. -species を使ってstringのflagをパースする。さらに "gopher" がデフォルト値になるようにする

  2. 2つのフラグ gopher_type , g が変数を共有することで、shorthandを実現する

  3. ユーザー定義のフラグタイプ 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関数を以下のように書き換え、パース結果の変数を表示してみます。

flag_ex.go
func main() {
	flag.Parse()

	fmt.Print("*species     = ")
	fmt.Println(species)
	fmt.Print("gopherType   = ")
	fmt.Println(gopherType)
	fmt.Print("intervalFlag = ")
	fmt.Println(intervalFlag)
}

実行スクリプトを用意します

flag_ex.sh
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. フラグを何も指定しない場合、デフォルト値が入る

  1. species: -species=supergopher
  2. gopherType: -gopher_type=Hedgehog or -g=Hedgehog
  3. intervalFlag: -deltaT=300ms,1h45m

3の intervalFlag は出力からはわかりづらいですが、フラグのパースのタイミングで各要素を string -> time.Duration へ変換しています。

前提

まずはパッケージ読み込み時に実行される部分

flag.go
// 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

まずはプログラム起動時の処理を追います。

flag_ex.go
var species = flag.String("species", "gopher", "the species we are studying")

Example1はシンプルで、 フラグ名: "species" , デフォルト値: "gopher" , usage: "the species we are studying" を用いて、 flag.String() を呼び出しているようです。
flag.String() を追っていきます。

flag.go
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()

flag.go
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()

flag.go
// 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も同じような流れをたどります。

次は実際のパース処理について追っていきます。

flag_ex.go
func main() {
	flag.Parse()
}

利用者側これだけです。
内部では、ここまでで準備してきた変数 CommandLine *FlagSet と、 os.Args を利用して、実際のパース処理を行います。

flag.go
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() が呼び出されるためと思われます。
独自で定義し、 errorHandlingContinueOnError をセットしている場合は、 (*FlagSet).Parse() のエラーは呼び出し側でハンドリングするべきでしょう。(どちらかというと呼び出し側でエラーをハンドリングするためにContinueOnErrorをセットするはず)

実際の各フラグのパース処理 parseOne() に入っていきます。
若干長いので、分割してみてみます。

flag.go
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"
flag.go
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.formalname をキーにした Flag がセットされているかをチェックし、セットされていない場合にヘルプの場合を除きエラーを返しています。

name   = "species=supergopher"
↓
name  = "species"
value = "supergopher"
flag.go
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 {
...
}

ここが実際に valueflag.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")
flag.go
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.actualflag をセットし、 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")

これでやっと事前準備したフラグセットに実際に渡されたフラグをパースしながらセットすることができました。

flag_ex.go
var species = flag.String("species", "gopher", "the species we are studying") // *species = "supergopher"

Example2

すみません。疲れました。
ですが、幸いここまでで2・3の中身も理解できるので、軽くだけ触ります。

flag_ex.go
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)")
}
flag.go
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

flag_ex.go
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")
}

独自型 intervalflag.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

ログインするとコメントできます