Go
golang
cli
Option
flag
Go3Day 5

Go標準のflagパッケージと比べてみようサードパーティflagライブラリ

tl;dr

GoでCLIのツールを作るとき、皆さんフラグをどう実装しますか?

標準のflagパッケージでシンプルにしますか?
それともCLIコマンドライブラリで複雑でリッチなモダンコマンドにしますか?

今回はいくつもあるCLIコマンドライブラリから、フラグを制御するライブラリに着目しようと思います。

まず手始めに標準のflagパッケージを見た上で、各ライブラリは何を解決したいのか。どのような方法で解決しようとしているのか。
この機会にちょっと眺めて遊んでみようと思います。

Go標準flagパッケージ

Go製のCLIツールを見ていくと、どうもサードパーティライブラリを使っている方が多く印象を受けます。
このコマンド、中身どうなっているかな。
と見に行くと、ほとんどのツールは標準flagパッケージではなく、サードパーティライブラリをつかっています。
Go公式ツールや、古くに作られたツールなどは標準のflagパッケージを使っていますね。

Go的にはやっぱり標準よ!という方が多い中で
現状こうなっていることを考えると、標準のflagパッケージには足りない部分があり、
足りない部分を補うため、ライブラリを使っているのではないでしょうか。

では具体的にflagパッケージではなにが出来てなにが出来ないのか、見ていきましょう。

オプションスタイル

flagパッケージを使っているGo公式コマンドを見ていきましょう。
以下はgofmtのhelpです。

usage: gofmt [flags] [path ...]
  -cpuprofile string
        write cpu profile to this file
  -d    display diffs instead of rewriting files
  -e    report all errors (not just the first 10 on different lines)
  -l    list files whose formatting differs from gofmt's
  -r string
        rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s    simplify code
  -w    write result to (source) file instead of stdout

ふむ、なかなかにシンプルで質実剛健といった感じですね。
参考にGNU grepのヘルプと比べてみましょう。

Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE.
Example: grep -i 'hello world' menu.h main.c

Pattern selection and interpretation:
  -E, --extended-regexp     PATTERN is an extended regular expression
  -F, --fixed-strings       PATTERN is a set of newline-separated strings
  -G, --basic-regexp        PATTERN is a basic regular expression (default)
  -P, --perl-regexp         PATTERN is a Perl regular expression
  -e, --regexp=PATTERN      use PATTERN for matching
  -f, --file=FILE           obtain PATTERN from FILE
  -i, --ignore-case         ignore case distinctions
  -w, --word-regexp         force PATTERN to match only whole words
  -x, --line-regexp         force PATTERN to match only whole lines
  -z, --null-data           a data line ends in 0 byte, not newline

Miscellaneous:
  -s, --no-messages         suppress error messages
  -v, --invert-match        select non-matching lines
  -V, --version             display version information and exit
      --help                display this help text and exit
...

割愛

ザラッと見てみると、grepのほうがしっかりした印象を受けます。
しかしその中でもとりわけ大きな差があるのにお気づきになったでしょうか?

そうロングスタイルオプションとショートスタイルオプションが分かれていないのです。

解説すると、
ショートオプションが-から始まり、一文字の英字から構成されるオプションです。-fとか
ロングスタイルオプションが--から始まり、複数の英字から構成されるオプションです。--fileとか
GNUコマンドが使っているオプション一覧があるので見てみるとわかりやすいし楽しいです。

Table of Long Options

話を戻しますがgofmtの-cpuprofile-(ハイフン)一つなのに複数英字構成となっていますよね。
つまりflagパッケージは、GNU/POSIXのコマンドラインスタイルとは異なる形式となっているわけです。

標準flagパッケージの使用方法

次はgofmtのソースコードを少し覗いてみましょう。
gofmtのソースはgolang/gosrc/cmd/gofmt配下にあります

以下はgofmtのフラグ定義となります。

var (
    // main operation modes
    list        = flag.Bool("l", false, "list files whose formatting differs from gofmt's")
    write       = flag.Bool("w", false, "write result to (source) file instead of stdout")
    rewriteRule = flag.String("r", "", "rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')")
    simplifyAST = flag.Bool("s", false, "simplify code")
    doDiff      = flag.Bool("d", false, "display diffs instead of rewriting files")
    allErrors   = flag.Bool("e", false, "report all errors (not just the first 10 on different lines)")

    // debugging
    cpuprofile = flag.String("cpuprofile", "", "write cpu profile to this file")
)

なおusageの出力は以下のようになっていました。ヘルプ表示時に以下がコールされているわけですね。

func usage() {
    fmt.Fprintf(os.Stderr, "usage: gofmt [flags] [path ...]\n")
    flag.PrintDefaults()
}

フラグの定義関数の中からflag.Boolの定義を見てみると、
フラグを構成する定義する項目がシンプルであることがわかります。

func Bool(name string, value bool, usage string) *bool

引数を見ると以下の3つからフラグが構築されていることになります

  • フラグ名
  • デフォルト値
  • ヘルプメッセージとして表示する使用方法

ロングオプションとショートオプションを、分けて入力するようにはなっていませんね。

試しに以下のように-を余分につけてみたらそれっぽくなったので、
ロングオプションとショートオプションを定義してどちらかを使用するように実装すればできなくはないですね。
(じつはもっと綺麗なやり方あるぞという方は教えていただければ...)

l           = flag.Bool("l", false, "list shorthand")
list        = flag.Bool("-list", false, "list files whose formatting differs from gofmt's")

しかし、これはあまり筋がいいとは言えませんね...

サードパーティライブラリ

上記で解説したとおり、flagパッケージはPOSIX/GNU-styleとして定義されたコマンドラインインタフェースの標準とは
ことなるフラグのデザインになるわけです。
よって多くのサードパーティライブラリは、POSIX/GNU-styleに準拠するために、
flagの再実装を行うという目的が共通項としてあるように感じます。

実際、サードパーティライブラリの一つであるpflagのREADMEには、

Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
pflag is compatible with the GNU extensions to the POSIX recommendations for command-line options.
See http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

POSIX/GNU-styleのオプションのGo実装であり、このスタイルに互換性があると書いてあります。

さて、それではサードパーティ製のライブラリがどのようにフラグを定義するのかを見てみましょう。

urfave/cli

  • シンプルかつ強力なコマンドラインツールライブラリ
  • フラグはそれぞれの型で用意されたstructを用いて定義

GoでCLIのライブラリといえばまずurfave/cliは外せませんね。
RubyならRails、PythonならDjango、PHPならLaravel、GoでCLI作るならurfave/cliですよ。
サブコマンドを実装するためという文脈で語られることが多いですが、
フラグ制御の仕方も、シンプルかつ強力な感じでGoodです。

実装例を見てみましょう(READMEから転載)

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "greet"
  app.Usage = "fight the loneliness!"
  app.Action = func(c *cli.Context) error {
    fmt.Println("Hello friend!")
    return nil
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

なんと、たったこれだけでコマンドの雛形が出来てしまいました。素晴らしい。
ヘルプメッセージは以下のような感じです。

$ greet help
NAME:
    greet - fight the loneliness!

USAGE:
    greet [global options] command [command options] [arguments...]

VERSION:
    0.0.0

COMMANDS:
    help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS
    --version Shows version information

さて肝心のフラグについてです。
urfave/cliではFlagは型毎にstructが用意されていおり、基本的にそこにフラグ定義を入れていき、
本体であるAppに設定するという流れです。
例えばhogeというフラグを設定するなら以下のような形ですね

    var hogeFlag string
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:        "hoge, H",
            Usage:       "hoge hoge hoge",
            EnvVar:      "HOGE",
            Hidden:      false,
            Value:       "defoge",
            Destination: &hogeDestination,
        },
    }

環境変数をデフォルト値として割当するEnvVar
隠しフラグとしてヘルプに表示しないHidden
変数にバインドするDestinationがにくいですね
Destinationなしでも、以下のようにフラグ名を指定して取得もできます。

    hogeFlag := c.String("hoge")

上記設定したフラグをヘルプ表示すると以下のようになります。

   --hoge value, -H value           hoge hoge hoge (default: "defoge") [$HOGE]

私は使ったことがないですが、bash completionもサポートしてるようです。

mow.cli

urfave/cliを意識して、よりベターなコマンドラインライブラリとして作成されたmow.cliなどもあります。

公式のREADMEにはurfave/cliとの比較表などあり、
より柔軟なフラグの組み合わせやArgsの制御をウリにしているようです。

go-flags

今回紹介している他のライブラリが変数にバインディングしたり、
単一の値を返す形式なのに対して、go-flags
フラグの集まりをstructとして定義し、そのstructの各変数に対してフラグの値をマッピングするのが特徴です。
structに対してフラグ用のタグを付加することで細やかな表示をするが出来ます。

package main

import (
    "bytes"
    "fmt"

    flags "github.com/jessevdk/go-flags"
)

type CreateOption struct {
    Title   string `short:"i" long:"title" value-name:"<title>" description:"The title of an issue"`
    Message string `short:"m" long:"message" value-name:"<message>" description:"The message of an issue"`
}

type ListOption struct {
    Num     int    `short:"n" long:"num" value-name:"<num>" default:"20" default-mask:"20" description:"Limit the number of issue to output."`
    State   string `long:"state" value-name:"<state>" default:"all" default-mask:"all" description:"Print only issue of the state just those that are \"opened\", \"closed\" or \"all\""`
    Scope   string `long:"scope" value-name:"<scope>" default:"all" default-mask:"all" description:"Print only given scope. \"created-by-me\", \"assigned-to-me\" or \"all\"."`
    OrderBy string `long:"orderby" value-name:"<orderby>" default:"updated_at" default-mask:"updated_at" description:"Print issue ordered by \"created_at\" or \"updated_at\" fields."`
    Sort    string `long:"sort"  value-name:"<sort>" default:"desc" default-mask:"desc" description:"Print issue ordered in \"asc\" or \"desc\" order."`
    Search  string `short:"s" long:"search"  value-name:"<search word>" description:"Search issues against their title and description."`
}

type Option struct {
    CreateOption *CreateOption `group:"Create Options"`
    ListOption   *ListOption   `group:"List Options"`
}

func newOptionParser(opt *Option) *flags.Parser {
    opt.CreateOption = &CreateOption{}
    opt.ListOption = &ListOption{}
    parser := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash)
    parser.Usage = `issue - Create and Edit, list a issue

Synopsis:
  # List issue
  lab issue [-n <num>] [--state=<state> | -o | -c] [--scope=<scope> | -r | -a] [-s]
            [--orderby=<orderby>] [--sort=<sort>] [-A]

  # Create issue
  lab issue [-e] [-i <title>] [-m <message>] [--assignee-id=<assignee id>]

  # Show issue
  lab issue <issue iid> [--no-comment]`
    return parser
}

func main() {
    buf := &bytes.Buffer{}
    var opt Option
    parser := newOptionParser(&opt)
    parser.WriteHelp(buf)
    fmt.Println(buf.String())
}

ヘルプメッセージは以下のようになっています。
struct毎にフラグをグルーピングしたり、その構造をヘルプに反映したりできます。

Usage:
  goflags issue - Create and Edit, list a issue

Synopsis:
  # List issue
  lab issue [-n <num>] [--state=<state> | -o | -c] [--scope=<scope> | -r | -a] [-s]
            [--orderby=<orderby>] [--sort=<sort>] [-A]

  # Create issue
  lab issue [-e] [-i <title>] [-m <message>] [--assignee-id=<assignee id>]

  # Show issue
  lab issue <issue iid> [--no-comment]

Create Options:
  -i, --title=<title>           The title of an issue
  -m, --message=<message>       The message of an issue

List Options:
  -n, --num=<num>               Limit the number of issue to output. (default: 20)
      --state=<state>           Print only issue of the state just those that are "opened", "closed" or "all" (default: all)
      --scope=<scope>           Print only given scope. "created-by-me", "assigned-to-me" or "all". (default: all)
      --orderby=<orderby>       Print issue ordered by "created_at" or "updated_at" fields. (default: updated_at)
      --sort=<sort>             Print issue ordered in "asc" or "desc" order. (default: desc)
  -s, --search=<search word>    Search issues against their title and description.

サンプルがやけに具体的なのは、私が作っているlabというコマンドで使ってるからです。
GitLabのCLIクライアントです。GitLabを使っているのであれば使ってみてください。

pflag

  • モダンなコマンドラインツールライブラリcobraで使用されているフラグライブラリ
  • 標準のflagパッケージを強く意識した使用感

cobraと一緒にくっついてくるフラグライブラリpflagです。
cobraといえば、Dockerでも使われているモダンなコマンドライブラリとして有名ですね。

pflagはベターflagライブラリとして、標準を差し替えるような形です。使用時のパッケージ名もflagですしね
使用感としては、flagと似たような形になっています。

ざっくり使うとこうなる

package main

import (
    "fmt"

    flag "github.com/spf13/pflag"
)

var helpFlag bool
var fooFlag string
var barFlag string

func main() {
    flag.BoolVarP(&helpFlag, "help", "p", false, "show help message")
    flag.StringVar(&fooFlag, "foo", "defoo", "help message foo")
    flag.StringVarP(&barFlag, "bar", "b", "debar", "help message bar")
    flag.Parse()

    if helpFlag {
        flag.PrintDefaults()
        return
    }

    fmt.Println(fooFlag, barFlag)
}

ヘルプメッセージはこんな感じです。

  -b, --bar string   help message bar (default "debar")
      --foo string   help message foo (default "defoo")
  -p, --help         show help message

また変数にフラグ値をバインディングせずに、そのまま値を返却することも可能です。
これは上記と同じ意味になります。

package main

import (
    "fmt"

    flag "github.com/spf13/pflag"
)

var (
    helpFlag = flag.BoolP("help", "p", false, "show help message")
    fooFlag  = flag.String("foo", "defoo", "help message foo")
    barFlag  = flag.StringP("bar", "b", "debar", "help message bar")
)

func main() {
    flag.Parse()

    if *helpFlag {
        flag.PrintDefaults()
        return
    }

    fmt.Println(*fooFlag, *barFlag)
}

ご覧の通り基本的な使用感は標準のflagパッケージと同様となります。
ここで注目したいのはfunc StringVarP(p *string, name, shorthand string, value string, usage string)などの型P形式の関数です。
flagパッケージの関数にshorthandつまりショートオプション用の引数が追加されています。

他にも非推奨フラグに対して警告を出す機能など運用に役立つような機能などがあります。

Kingpin

  • メソッドチェーンによるフラグの属性付加が特徴的
  • なんかいろいろな形式の文字列をパースしたりできる模様

この記事を書くまで以前は触ったことがなかったのですが、
今回触ってみて意欲的なコンセプトなライブラリだと感じました。
メソッドチェーンによってフラグの機能を追加していく。とてもおもしろいですね。
2017年末以降にCommitが止まっているのが残念です。

フラグの設定は以下のようになります

package main

import (
  "fmt"

  "gopkg.in/alecthomas/kingpin.v2"
)

var (
  debug   = kingpin.Flag("debug", "Enable debug mode.").Bool()
  timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
  ip      = kingpin.Arg("ip", "IP address to ping.").Required().IP()
  count   = kingpin.Arg("count", "Number of packets to send").Int()
)

func main() {
  kingpin.Version("0.0.1")
  kingpin.Parse()
  fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count)
}

ヘルプメッセージは以下のようになります。

usage: kingpin [<flags>] <ip> [<count>]

Flags:
      --help        Show context-sensitive help (also try --help-long and --help-man).
      --debug       Enable debug mode.
  -t, --timeout=5s  Timeout waiting for ping.
      --version     Show application version.

Args:
  <ip>       IP address to ping.
  [<count>]  Number of packets to send

他ライブラリではパースを実施し、その後に引数を引き渡して後はよしなに。
といった形式が多いのですが、kingpinは引数の形式も指定することができます。

さらに多数の便利機能が魅力で、以下のような特定の文字列をパースまで対応することができます。

  • IP(1.1.1.1)
  • TCP(1.1.1.1:3333)
  • File(実在するファイル)
  • Enum(特定の文字列)

flaggy

  • フラグライブラリとしては後発
  • シンプルで余分な機能がないフラグとしての機能に注力

flaggyは割と後発のライブラリなのですが、
もろもろ便利にしようぜというフラグライブラリとは逆に、余分な機能を削ぎ落としたシンプル・イズ・ベスト方面です。
以下のようなシンプル構造。

var stringFlag = "defaultValue"
flaggy.String(&stringFlag, "f", "flag", "A test string flag")

フラグ定義関数の中にデフォルト値がないのが印象的ですね。

func String(assignmentVar *string, shortName string, longName string, description string)

まとめ

まったく。フラグは奥がふかいなぁ。
一回見始めたら中身のコードまで気になってしまってあまり文量が書けなかった...反省