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コマンドが使っているオプション一覧があるので見てみるとわかりやすいし楽しいです。
話を戻しますがgofmtの-cpuprofile
は-
(ハイフン)一つなのに複数英字構成となっていますよね。
つまりflagパッケージは、GNU/POSIXのコマンドラインスタイルとは異なる形式となっているわけです。
標準flagパッケージの使用方法
次はgofmtのソースコードを少し覗いてみましょう。
gofmtのソースはgolang/goのsrc/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)
まとめ
まったく。フラグは奥がふかいなぁ。
一回見始めたら中身のコードまで気になってしまってあまり文量が書けなかった...反省