フラミナル

考え方や調べたことを書き殴ります。IT技術系記事多め

pflag がどのように args を取得しているのかを追う

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 パッケージでは FlagSetFlag で成り立っている。

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() では全ての引数を FlagSetParse メソッドに渡している。

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 では受け取った引数(配列)を取得し、値によって FlagSetArgs にいれたり

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
}