Go言語(golang) flagパッケージでコマンドライン引数をパース
Go言語の標準ライブラリ flag パッケージを使ったコマンドラインオプション(フラグ)の基本的な解析方法と、 独自の型(例えば、URL や JSON など)を扱えるようにするための実装方法
flagパッケージ
目次
引数の在り処
実行ファイルに渡したコマンドライン引数は os.Args
に配列で入っている。
0 番目は実行したファイルのパスで、それ以降が渡した引数。
package main import ( "fmt" "os" ) func main() { for i, v := range os.Args() { fmt.Println(i, v) } }
上の例に、適当な引数を渡して実行すると以下のようになる。
$ go run main.go foo bar baz 0 /tmp/go-build319372531/b001/exe/main 1 foo 2 bar 3 baz
flag パッケージの基本的な使い方
解析にはflag.Parse
を使う。flag.Parse
の内部では os.Args[1:]
が渡されて解析される。
パース後、オプションとその値以外は flag.Args
に残りとして入っている。
package main import ( "flag" "fmt" ) func main() { flag.Parse() fmt.Println(flag.Args()) // 残りの引数 }
実行結果
$ go run main.go foo bar baz [foo bar baz]
オプション(フラグ)の作成方法
flag.String
や flag.StringVar
のように型名で関数が定義されている。
関数の引数は flag.String("オプション名", "初期値", "説明")
+ オプション名: ここに指定した名前を go run main.go --オプション名=値
のように使うことができる
+ 初期値: 作成したオプションが使われなかった場合に 変数に入ってる値
+ 説明: $ go run main.go --help
としたときに表示される各オプションの説明文
文字列型(String) の他にも Bool, Int, Duration などがある。(他の型については flag パッケージのドキュメント を参照)
package main import "flag" func main() { str := flag.String("string", "", "description") flag.Parse() fmt.Println(*str) }
上記のように記述しオプションを指定した場合は、変数(str)に値が入っていて、指定しなかった場合は
第二引数で与えた初期値(""
)が入っている。ポインタ変数なので *str
で使う。
実行結果
$ go run main.go --string hello hello
flag.xxxVar
の様に Var
で終わる関数にはアドレスで渡すことで、その変数に値が入る。
関数の引数は flag.StringVar(変数, "オプション名", "初期値", "説明")
オプションを追加する関数は、先程の flag.String
のように戻り値で値を返す関数と
このflag.StringVar
のように引数に変数を渡す関数の二種類。
package main import "flag" func main() { var str string flag.StringVar(&str, "string", "", "オプションの説明") flag.Parse() fmt.Println(str) }
実行結果
$ go run main.go --string hello hello
オプションは -option
または --option
のように、先頭に -
を付ける。1つでも2つでもオプションとみなされる。
bool型以外のオプションには必ず値が必要で --option value
か --option=value
で値を渡す。
bool型のオプションを指定した場合は値を指定しなくてもtrue
がセットされる。
真偽値型に明示的に値を渡すには =
で指定しない限り、そのオプションの値とみなされない。
(明示的に値を指定するには --boolean=true
とする)
-h
と --help
は、ヘルプメッセージを表示するためのオプションで関数に渡した説明
などがまとめて表示される。
これは、flagパッケージ内部で予め決められてる(上書きすることもできる)。https://golang.org/src/flag/flag.go#L922
具体的な使い方
package main import ( "flag" "fmt" "os" ) var ( bool_flag bool string_flag string int_flag int ) func init() { // 初期値で良ければ flag.CommandLine.Init を使う必要はない // 第一引数: コマンド名の指定。初期値は os.Args[0] // 第二引数: エラーが発生した場合の挙動。初期値は ExitOnError flag.CommandLine.Init("command name", flag.ContinueOnError) // ヘルプメッセージに何か付け足したい場合など Usage に関数を設定する flag.CommandLine.Usage = func() { o := flag.CommandLine.Output() fmt.Fprintf(o, "\nUsage: %s\n", flag.CommandLine.Name()) fmt.Fprintf(o, "\nDescription: Command description etc.\n\nOptions:\n") flag.PrintDefaults() fmt.Fprintf(o, "\nCopyright 2018 xxx.\n\n") } // 引数は、変数、名前、初期値、説明 flag.BoolVar(&bool_flag, "boolean", false, "description") flag.StringVar(&string_flag, "string", "", "description") flag.IntVar(&int_flag, "integer", 0, "description") // 戻り値で受け取りたい場合には flag.Bool のように Var なしを使う // bool_flag := flag.Bool("boolean", flase, "description") } func main() { // ContinueOnError に設定したので、 // flag.Parse ではなく flag.CommandLine.Parse を使ってエラー処理する if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { if err != flag.ErrHelp { fmt.Fprintf(os.Stderr, "error: %s\n", err) } os.Exit(2) } fmt.Println("boolean:", bool_flag) fmt.Println("string:", string_flag) fmt.Println("integer:", int_flag) fmt.Println("Args:", flag.Args()) }
実行結果
$ go run main.go --string-flag hello int-flag=20 foo bar baz boolean: false string: hello integer: 20 Args: [foo bar baz]
golang の flag パッケージで面倒だったり、不便に感じること
- 引数がオプションではないとき解析処理はそこで終了する。最後まで処理させるには、以下のようにする。
package main import ( "flag" "fmt" "log" "os" ) var ( boolOpt bool strOpt string ) func init() { flag.StringVar(&strOpt, "str", "", "description") flag.BoolVar(&boolOpt, "bool", false, "description") } func main() { remain := make([]string, 0, len(os.Args[1:])) args := os.Args[1:] for len(args) > 0 { err := flag.CommandLine.Parse(args) if err != nil { log.Fatal(err) } if flag.NArg() == 0 { break } args, remain = flag.Args()[1:], append(remain, flag.Arg(0)) } fmt.Println(remain) fmt.Println(strOpt) fmt.Println(boolOpt) }
- --option, -option という使い方はできるが、-o といった短い名前は別で設定する必要がある
var ( option bool ) func main() { // 同じ変数を使って、長い名前と、短い名前を設定する // ヘルプに 2つ分表示されることになるので、イマイチ flag.BoolVar(&option, "option", false, "flag of option") flag.BoolVar(&option, "o", false, "flag of option alias") flag.Parse() }
他の言語でできるような POSIXスタイルの(optional,required,no_requiredなども)指定方法が使えなかったり、
少し不便な部分はあるけど、その分処理が高速っぽい。
コマンドラインパーサーは GitHub にサードパーティ製のパッケージがいっぱいあるので、それらを使ってもいいかも。
FlagSetの使い方
flag.CommandLine
は flag.NewFlagSet(os.Args[0], flag.ExitOnError)
としてるだけなので flag.Bool
などの使い方は同。
package main import ( "flag" "fmt" "os" ) func main() { command := flag.NewFlagSet("command name", flag.ExitOnError) version := command.Bool("version", false, "Output version information and exit") command.Parse(os.Args[1:]) if *version { fmt.Fprintf(os.Stderr, "%s v0.0.1", command.Name()) } }
FlagSet を使ってサブコマンドを設定する
以下の例では、サブコマンドが指定された場合のみサブコマンドとして処理され、
--help
も $ command subcommand --help
とすれば、そのサブコマンドのヘルプのみ表示される。
$ command -name Tanaka subcommand ...
のように共通するフラグを使うこともできる
package main import ( "flag" "fmt" "os" ) type Add struct { FlagSet *flag.FlagSet Verbose bool Force bool } type Commit struct { FlagSet *flag.FlagSet Verbose bool Signoff bool } type Options struct { Add Add Commit Commit Version bool // サブコマンド含め全てに共通するフラグ UserName string } var ( o Options ) func init() { flag.CommandLine.Init("command", flag.ExitOnError) o.Add.FlagSet = flag.NewFlagSet("command add", flag.ExitOnError) o.Add.FlagSet.BoolVar(&o.Add.Verbose, "v", false, "describe") o.Add.FlagSet.BoolVar(&o.Add.Force, "f", false, "describe") o.Commit.FlagSet = flag.NewFlagSet("command commit", flag.ExitOnError) o.Commit.FlagSet.BoolVar(&o.Commit.Verbose, "v", false, "describe") o.Commit.FlagSet.BoolVar(&o.Commit.Signoff, "s", false, "describe") flag.BoolVar(&o.Version, "version", false, "output version information and exit") flag.StringVar(&o.UserName, "name", "", "describe") } func main() { flag.Parse() if o.Version { fmt.Fprintf(os.Stderr, "%s v0.0.1\n", flag.CommandLine.Name()) os.Exit(2) } if flag.NArg() > 0 { args := flag.Args() switch args[0] { case "add": o.Add.FlagSet.Parse(args[1:]) case "commit": o.Commit.FlagSet.Parse(args[1:]) } } fmt.Println("Commit - verbose:", o.Commit.Verbose, ", signoff:", o.Commit.Signoff) fmt.Println("Add - verbose:", o.Add.Verbose, ", force:", o.Add.Force) fmt.Println("UserName:", o.UserName) }
実行結果
$ go run main.go --name tanaka commit -v -s Commit - verbose: true , signoff: true Add - verbose: false , force: false UserName: tanaka
独自の型実装
flagパッケージのリポジトリを見ると example_value_test.go というファイルがある。
その中でURL型としてオプションの引数をパースする方法が書かれてあるので、それをもとにした JSON での実装例。
package main import ( "encoding/json" "flag" "fmt" ) type User struct { Name string `json:"name"` Age int `json:"age"` } type JSONValue struct { User *User } func (v JSONValue) String() string { if v.User != nil { b, _ := json.Marshal(v.User) return string(b) } return "" } func (v JSONValue) Set(s string) error { if err := json.Unmarshal([]byte(s), v.User); err != nil { return err } return nil } func main() { u := &User{} j := JSONValue{u} fs := flag.NewFlagSet("command", flag.ExitOnError) fs.Var(&j, "json", "JSON Unmarshal") fs.Parse([]string{"-json", `{"name": "Ohtani", "age": 24}`}) fmt.Printf("User: %#v\n", u) // &main.User{Name:"Ohtani", Age:24} fmt.Printf("String: %s\n", j.String()) // {"name":"Ohtani","age":24} }