Go の pflag パッケージってどうやって引数を処理してるんだろうが気になったので、ちょっと調べてみました。
引数を配列にして返すだけのコードを書いてみます。
package main import ( "fmt" flag "github.com/spf13/pflag" ) type Args struct { Paths []string } func main() { args := Args{} // os.Args[1:] を取得して、フラグを解析しています。 // これより前にフラグのセットを終える必要がある flag.Parse() args.Paths = flag.Args() fmt.Println(args.Paths) }
❯ go run main.go /tmp . [/tmp .]
どのように args を処理しているかを追ってみます。
結論(個人備忘)
pflag パッケージでは FlagSet
と Flag
で成り立っている。
Flag
がどのようなオプションを許容し、デフォルトパラメータや型情報などオプションに必要な全てを管理する構造体。FlagSet
は複数の Flag
を束ね、全体的に利用するオプションに紐づかない引数や設定などを持つ構造体。
ユーザは好きな Flag
を定義し FlagSet
に登録する。Flag
にユーザが管理するポインタ変数を渡す(もしくは Flag
登録時の戻り値を管理する)ことで、コマンドライン引数を取得することができる。
コマンドライン引数は POSIX 準拠した GNU 拡張と互換性がある。(go の flag パッケージは互換がない)
各パッケージの違いをみるならこれがまとまってる
コードリーディング
ここでは CommandLine.args を返してるだけ。
// Args returns the non-flag command-line arguments. func Args() []string { return CommandLine.args }
CommandLine
はここで定義されている。
os.Args[0]
(プログラム本体のパスを指し示す) とエラーハンドリングを渡して、NewFlagSet
を呼び出している。
// CommandLine is the default set of command-line flags, parsed from os.Args. var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
NewFlagSet
では FlagSet
を定義している。受け取った実行ファイルパスとエラーハンドリング方法をセットして FlagSet
インスタンスを生成し返す。
// NewFlagSet returns a new, empty flag set with the specified name, // error handling property and SortFlags set to true. func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet { f := &FlagSet{ name: name, errorHandling: errorHandling, argsLenAtDash: -1, interspersed: true, SortFlags: true, } return f }
FlagSet
の定義はこれ。今回気にしているのは args
なのでどこでセットしているか探す。
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. Usage func() // SortFlags is used to indicate, if user wants to have sorted flags in // help/usage messages. SortFlags bool // ParseErrorsWhitelist is used to configure a whitelist of errors ParseErrorsWhitelist ParseErrorsWhitelist name string parsed bool actual map[NormalizedName]*Flag orderedActual []*Flag sortedActual []*Flag formal map[NormalizedName]*Flag orderedFormal []*Flag sortedFormal []*Flag shorthands map[byte]*Flag args []string // arguments after flags argsLenAtDash int // len(args) when a '--' was located when parsing, or -1 if no -- errorHandling ErrorHandling output io.Writer // nil means stderr; use out() accessor interspersed bool // allow interspersed option/non-option args normalizeNameFunc func(f *FlagSet, name string) NormalizedName addedGoFlagSets []*goflag.FlagSet }
Flag ではかならず Parse()
を実行する必要がある。 Parse()
では全ての引数を FlagSet
の Parse
メソッドに渡している。
func Parse() { // Ignore errors; CommandLine is set for ExitOnError. CommandLine.Parse(os.Args[1:]) }
FlagSet.Parse()
では受け取った引数を f.parseArgs
に渡している。
func (f *FlagSet) Parse(arguments []string) error { if f.addedGoFlagSets != nil { for _, goFlagSet := range f.addedGoFlagSets { goFlagSet.Parse(nil) } } f.parsed = true if len(arguments) < 0 { return nil } f.args = make([]string, 0, len(arguments)) set := func(flag *Flag, value string) error { return f.Set(flag.Name, value) } err := f.parseArgs(arguments, set) if err != nil { switch f.errorHandling { case ContinueOnError: return err case ExitOnError: fmt.Println(err) os.Exit(2) case PanicOnError: panic(err) } } return nil }
f.parseArgs
では受け取った引数(配列)を取得し、値によって FlagSet
の Args
にいれたり
func (f *FlagSet) parseArgs(args []string, fn parseFunc) (err error) { for len(args) > 0 { s := args[0] args = args[1:] // s が空 or s の1文字目が - じゃない or sの長さが1の時は // flagset の Args に引数を突っ込む。このとき intersepersed が無効なときは // s 以降の args もすべて parse する必要はないとみなして args 入れる。 // 例:ls /tmp -l の時、interspersed = false なら flagset の Args は ["/tmp", "-l"] となる // ls /tmp -l の時、interspersed = true なら flagset の Args は ["/tmp"] となる if len(s) == 0 || s[0] != '-' || len(s) == 1 { if !f.interspersed { f.args = append(f.args, s) f.args = append(f.args, args...) return nil } f.args = append(f.args, s) continue } // --xxx のような dobule dash な場合は parseLongArg で処理 if s[1] == '-' { if len(s) == 2 { // "--" terminates the flags f.argsLenAtDash = len(f.args) f.args = append(f.args, args...) break } args, err = f.parseLongArg(s, args, fn) } else { // -xxx のような single dash な場合は parseShortArg で処理 args, err = f.parseShortArg(s, args, fn) } if err != nil { return } } return }
parseLongArg
では 引数1 、残りの引数の配列、 FlagSet
内の Flag
に値をセットする関数を渡して
引数の解釈と FlagSet
への登録を行う。(そもそもどのような Flag
があるのか?は利用者が事前に flag
パッケージに登録しておく)
※例:flag.BoolVarP(&flagvar, "boolname", "b", true, "help message")
など
func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []string, err error) { a = args // --project のときの -- を外し name=project にする name := s[2:] if len(name) == 0 || name[0] == '-' || name[0] == '=' { err = f.failf("bad flag syntax: %s", s) return } // project=ProjectA のときに = で split して、["project","ProjectA"] を返す // project ProjectA ならそもそも name に project が入っているのでなにもせず ["project"] を返す split := strings.SplitN(name, "=", 2) name = split[0] // flag, exists := f.formal[f.normalizeFlagName(name)] if !exists { switch { case name == "help": f.usage() return a, ErrHelp case f.ParseErrorsWhitelist.UnknownFlags: // --unknown=unknownval arg ... // we do not want to lose arg in this case if len(split) >= 2 { return a, nil } return stripUnknownFlagValue(a), nil default: err = f.failf("unknown flag: --%s", name) return } } var value string if len(split) == 2 { // '--flag=arg' value = split[1] } else if flag.NoOptDefVal != "" { // '--flag' (arg was optional) value = flag.NoOptDefVal } else if len(a) > 0 { // '--flag arg' value = a[0] a = a[1:] } else { // '--flag' (arg was required) err = f.failf("flag needs an argument: %s", s) return } err = fn(flag, value) if err != nil { f.failf(err.Error()) } return }