flag 並にシンプルでより強力な CLI パーサ kingpin の紹介

  • 79
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Go Advent Calendar 2015 その3 14日目です。

はじめに

golang でコマンドラインアプリケーションを実装する際、標準パッケージとして提供されている flag パッケージを使えば、アプリケーションが受け取る引数に意味付けをして値を指定することができるようになります。

flagを使ったコード
package main

import (
    "flag"
    "fmt"
)

var (
    verbose = flag.Bool("verbose", false, "Set verbose mode")
    count   = flag.Int("count", 0, "counter")
)

func main() {
    flag.Parse()

    args := flag.Args()
    if len(args) < 1 {
        fmt.Println("Error: args <name> is required.")
        return
    }

    name := &args[0]

    fmt.Printf("verbose mode: %v, count: %d, name: %s\n", *verbose, *count, *name)
}
$ go run ex_flag.go --count 10 --verbose hoge
verbose mode: true
count: 10
args: [hoge]


$ go run ex_flag.go --help
Usage of /var/folders/g4/84fwc06x607dq3t5p_zvxxbw0000gn/T/go-build135375912/command-line-arguments/_obj/exe/ex_flag:
  -count int
        counter
  -verbose
        Set verbose mode
exit status 2


$ go run ex_flag.go --count string hoge
invalid value "string" for flag -count: strconv.ParseInt: parsing "string": invalid syntax
Usage of /var/folders/g4/84fwc06x607dq3t5p_zvxxbw0000gn/T/go-build185516462/command-line-arguments/_obj/exe/ex_flag:
  -count int
        counter
  -verbose
        Set verbose mode
exit status 2


$ go run ex_flag.go
Error: args <name> is required.

コマンドラインアプリケーションで扱われるフラグの簡易的なヘルプ出力、そしてフラグの値に対する型チェック (上記の例だと、3つめのコマンド実行時に Int 宣言をしている count フラグに文字列を与えているので invalid value と言われている) までが大変シンプルなインターフェースで利用可能になっています。

とはいうものの flag パッケージがフォーカスしているポイントはフラグ値であって、通常の引数部分についてはただのリストです。型や名付けなど引数にも意味づけしたい場合は、別途自分で処理する必要があります。

kingpin

https://github.com/alecthomas/kingpin

kingpin はコマンドラインのフラグおよび引数の両方に意味付けを行い、その値を安全に取り扱うことができるようになるライブラリです。

以下のコード例は、先ほどの flag パッケージのコード例と同じような値を受け取るコマンドラインアプリケーションを kingpin を使って書いたものです。flag パッケージと違いフラグと引数の区別が付いているためインターフェースは異なっていますが、要素を1つずつ定義していくスタイルは似ています。

kingpinを使ったコード例
package main

import (
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    verbose = kingpin.Flag("verbose", "Set verbose mode").Bool()
    count   = kingpin.Flag("count", "counter").Int()
    name    = kingpin.Arg("name", "Input name").Required().String()
)

func main() {
    kingpin.Parse()
    fmt.Printf("verbose mode: %v, count: %d, name: %s\n", *verbose, *count, *name)
}

実行してみると、先ほどの flag パッケージの例と比べてヘルプの出力内容がリッチになったこと、そして引数で指定する name 要素が必須であることをエラーとして示す振る舞いが追加されていることがわかります。

$ go run main.go --count 10 --verbose hoge
verbose mode: true, count: 10, name: hoge


$ go run main.go --help
usage: main [<flags>] <name>

Flags:
  --help         Show context-sensitive help (also try --help-long and --help-man).
  --verbose      Set verbose mode
  --count=COUNT  counter

Args:
  <name>  Input name

exit status 1


$ go run main.go --count string hoge
main: error: strconv.ParseFloat: parsing "string": invalid syntax, try --help
exit status 1


$ go run main.go
main: error: required argument 'name' not provided, try --help
exit status 1

Short, Long フラグ

flag パッケージを使って同一設定値に相当する Short, Long フラグを設けたい場合、パッケージには直接的な機能がないため、以下のようなコードが必要でした。

flagによるshort,long
package main

import (
    "flag"
    "fmt"
)

var (
    dryRun = flag.Bool("dry-run", false, "Dry run")
)

func init() {
    flag.BoolVar(dryRun, "n", false, "Dry run")
}

func main() {
    flag.Parse()
    fmt.Println(*dryRun)
}

kingpin では定義するフラグ値に Short, Long それぞれの名称を設定する機能がありますので、コードを見た際の意図が明確になります。

kingpinによるshort,long
package main

import (
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    dryRun = kingpin.Flag("dry-run", "Dry run").Short('n').Bool()
)

func main() {
    kingpin.Parse()
    fmt.Println(*dryRun)
}

いろんな型、リスト

kingpin は、flag パッケージで利用可能な型以上に様々な型をフラグ、引数に宣言して定義することができます。例えば以下のコードの例では、引数が Unsigned 8-bit integer (uint8) のリストとして扱っています。

引数にuint8のリストを受け取る
package main

import (
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    numbers = kingpin.Arg("numbers", "some numbers").Uint8List()
)

func main() {
    kingpin.Parse()
    fmt.Printf("numbers: %v", *numbers)
}

実行結果は以下のとおりです。引数として指定した任意数の数値が []uint8 numbers に代入されます。型に一致しない引数を指定した場合はエラー扱いになり処理は行われません。

$ go run main.go 27 42 133
numbers: [27 42 133]

$ go run main.go 010 0xff
numbers: [8 255]

$ go run main.go 42 256
main: error: strconv.ParseUint: parsing "256": value out of range, try --help
exit status 1

ファイルを扱う

コマンドラインアプリケーションでよくあるケースとして「指定したパスのファイルやディレクトリについて処理する」ようなものがありますが、kingpin ではパースするタイプの1つにファイルタイプのようなものが用意されています。これを利用することで、フラグや引数に与えられた値をファイルパスとして扱うと共にそのままファイル処理を開始することができます。

以下のコードは引数にファイルパスを与えるとその内容を出力するというものです。
ポイントとしては必須項目の引数としてファイルタイプを指定していること、そしてパースに成功した場合は Open() することなくそのまま読み込み処理を開始しているところです。

引数に与えられたパスのファイルを読み込んで出力
package main

import (
    "bufio"
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    file = kingpin.Arg("file", "Input filename").Required().File()
)

func main() {
    kingpin.Parse()

    defer func() {
        (*file).Close()
    }()

    scanner := bufio.NewScanner(*file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

実行してみると、実在するファイルの場合は内容を出力し、存在しないファイルは引数読み込み時にエラーとなって処理が終了しています。

$ cat ./1.txt
Go, GO !

$ go run main.go ./1.txt
Go, GO !

$ go run main.go ./not_exists.txt
main: error: open ./not_exists.txt: no such file or directory, try --help
exit status 1

ファイルをオープンするタイミングは自分で決めたい、あるいはオープンの必要がないケース向けに、別途 ExistingFile() という関数も用意されており、こちらはパース後の変数にファイルパスが文字列としてセットされます。リストで扱う ExistingFiles() という関数もあわせて用意されています。

実在するファイルパスを出力
package main

import (
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    files = kingpin.Arg("name", "Input name").Required().ExistingFiles()
)

func main() {
    kingpin.Parse()

    for _, file := range *files {
        fmt.Printf("Exists file: %s\n", file)
    }
}
$ ls
./  ../  1.txt  2.txt  main.go

$ go run main.go 1.txt 2.txt
Exists file: 1.txt
Exists file: 2.txt

$ go run main.go 1.txt 3.txt
main: error: path '3.txt' does not exist, try --help
exit status 1

サブコマンド

kingpin はサブコマンドを持ったコマンドラインアプリケーションを作ることもできます。
自分で1から実装するのならば、受け取った引数のうちのある順番に指定されているコマンド名にマッチした処理を行うような分岐処理を書くことになりますが、kingpin ならば 先ほどまでのフラグや引数の定義と同じように、Command() 関数でサブコマンドの定義を行っていきます。
定義したサブコマンドごとにフラグや引数の内容を変えることも可能ですし、定義したサブコマンドにさらにサブコマンドを定義することもできるため、非常に柔軟でリッチなインターフェースを持ったコマンドラインアプリケーションが作れそうです。

いくつかのサブコマンドを指定して実行するCLIアプリケーション
package main

import (
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
    "os"
)

var (
    app = kingpin.New("manager", "Management command")

    itemCommand = app.Command("item", "Item management")
    // item add
    itemAdd     = itemCommand.Command("add", "Add item")
    itemAddName = itemAdd.Flag("name", "item name").Required().String()
    // item del
    itemDel      = itemCommand.Command("del", "Delete item")
    itemDelName  = itemDel.Flag("name", "item name").Required().String()
    itemDelForce = itemDel.Flag("force", "force delete").Short('f').Bool()

    personCommand = app.Command("person", "Person management")
    // person add
    personAdd     = personCommand.Command("add", "Add person")
    personAddName = personAdd.Flag("name", "person name").Required().String()
)

func main() {
    switch kingpin.MustParse(app.Parse(os.Args[1:])) {
    case itemAdd.FullCommand():
        fmt.Printf("[Add item] name: %s\n", *itemAddName)
    case itemDel.FullCommand():
        forceDelete := ""
        if *itemDelForce {
            forceDelete = " (force)"
        }

        fmt.Printf("[Delete item%s] name: %s\n", forceDelete, *itemDelName)
    case personAdd.FullCommand():
        fmt.Printf("[Add person] name: %s\n", *personAddName)
    }
}
$ go run main.go item add --name gopher
[Add item] name: gopher

$ go run main.go person add --name kumatch
[Add person] name: kumatch

$ go run main.go item del --name dman -f
[Delete item (force)] name: dman

サブコマンドが実装されたコマンドラインのヘルプも、大変充実した内容を生成、出力してくれます。以下の例のように、ヘルプを見るサブコマンドの階層ごとに適切な内容が掲載されるようになっています。

$ go run main.go help
usage: manager [<flags>] <command> [<args> ...]

Management command

Flags:
  --help  Show context-sensitive help (also try --help-long and --help-man).

Commands:
  help [<command>...]
    Show help.

  item add --name=NAME
    Add item

  item del --name=NAME [<flags>]
    Delete item

  person add --name=NAME
    Add person


$ go run main.go help item
usage: manager item <command> [<args> ...]

Item management

Flags:
  --help  Show context-sensitive help (also try --help-long and --help-man).

Subcommands:
  item add --name=NAME
    Add item

  item del --name=NAME [<flags>]
    Delete item



$ go run main.go help item del
usage: manager item del --name=NAME [<flags>]

Delete item

Flags:
      --help       Show context-sensitive help (also try --help-long and --help-man).
      --name=NAME  item name
  -f, --force      force delete

まとめ

golang で実装するコマンドラインアプリケーションのフラグ、引数の値をシンプルながらも強力で安全に取り扱うことができるようになる kingpin を紹介しました。

golang 標準パッケージの flag ではコマンドラインが受け取るフラグ値の型定義のみになりますが、kingpin ならば引数も同じように扱えるだけでなく、定義可能な型がより多く用意されています。

またサブコマンドを持ったコマンドラインアプリケーションも作成することができます。サブコマンドごとに受け取るフラグ、引数の数や意味を変えることができるなど柔軟な仕組みを持っています。