golangの日記

Go言語を中心にプログラミングについてのブログ

Go言語(golang) flagパッケージでコマンドライン引数をパース

golang.png


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.Stringflag.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.CommandLineflag.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}
}