Golangで普通の文字列をflagで解析する

小ネタです。Goのflagパッケージを使えば、コマンドライン引数の解析を簡単にできます。プログラムに与えられた言わば本当のコマンドライン引数(つまりos.Args)ではなく、ただのstringをコマンドライン引数として解析する方法について書きます。

結論

表題の課題は、次のようなコードで実現できます。

  • main.go
package main

import (
        "flag"
        "fmt"
)

func main() {
        f := flag.NewFlagSet("command", flag.ExitOnError)
        str := f.String("str", "default value", "")
        f.Parse([]string{"-str=aaaa"})
        fmt.Println("str: ", *str)
}
  • 実行
$ go run main.go 
str:  aaaa

flagパッケージを読んでみる

まず、本当のコマンドライン引数を解析する例を見てみましょう。

  • main.go
package main

import (
        "flag"
        "fmt"
)

func main() {
        str := flag.String("str", "default value", "")
        flag.Parse()
        fmt.Println("str: ", *str)
}
  • 実行
$ ./out 
str:  default value
$ ./out -str=aaaa
str:  aaaa

コマンドライン引数strが解釈されていることがわかります。しかしよく見てみると、os.Argsを明示するようなコードは書いていませんね。ということはflagパッケージの中に、os.Argsを読んでそのstringを解釈するような処理があるはずです。

実際にflagパッケージを読んでみます。 https://golang.org/src/flag/flag.gogolang.org

まず、StringParseを読むと、それぞれCommandLine.StringCommandLine.Parseを内部で呼んでいます。 ここでCommandLineグローバル変数で以下のように初期化されています。

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

さらにNewFlagSetを追います。

func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet

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.

どうやら名前とエラーハンドリングを指定したフラグセットというものを返す関数のようです。つまり変数CommandLineos.Args[0](実行ファイルの名前)が指定されることになります。

ここで再度Parseの実装を見てみます。

// Parse parses the command-line flags from os.Args[1:]. Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(os.Args[1:])
}

https://golang.org/src/flag/flag.go?s=33956:33968#L1010

CommandLine.Parseで呼ばれている関数は以下のように定義されています。

func (f *FlagSet) Parse(arguments []string) error

以上のことから、変数CommandLineos.Args[0]を元にNewFlagSetで初期化され、os.Args[1:]Parseされることがわかります。

よって、普通の文字列をflagで解析するには、NewFlagSetで新たなフラグセットをつくり、(f *FlagSet) Parseに解析したい[]stringを渡せばいいことがわかりました。