業務でちょっとした検証用のツールや、調査用のコマンドラインアプリケーションを Go 言語で作成することがよくあります。
基本自分しか使わないのですが、何かきっかけでスクリプトを他の人に配ることもあり、このとき main.go のみで完結するようにしておくと、人に配るときや何らかの証跡と一緒に残しておくさいもテキストファイル一つ渡せば済むので大変便利です。
また、配布先の人がビルドする時もトラブルが圧倒的に少ないです。
このため、外部のライブラリが必ず必要という状況でない限りは、できるだけ Go 標準ライブラリだけでコマンドラインアプリケーションを作成するようにしています。
ただ、Go 標準の flag パッケージはコマンドライン引数のパースを簡単に実装できる反面、 Usage の出力があまり綺麗ではなかったり、ロングパラメータを扱うのが難しいなど色々使い勝手が悪い部分があります。
これまで、 spf13/cobra や urfave/cli のような外部ライブラリを使わずに、どうにしか標準の flag パッケージだけで Usage を綺麗にしたりロングパラメータを扱えるようにしたりできないか試行錯誤したので、その実装方法について何回かに分けて紹介したいと思います。
今回は「 Usage を綺麗に整形して見やすくする 」方法について紹介します。 ここで紹介する方法(以降、呼びやすさのために「 Custom Usage 」と表現します)を使うと、標準の flag パッケージだけで以下のような綺麗な Usage を出力できるようになります。
$ go run main.go -h
Usage: main [OPTIONS]
Description:
A sample command demonstrating simple template usage in Go CLI applications.
Options:
-f <string> (required) File path
-d Debug mode
-l <int> Index of line (default 10)
-u <string> URL (default "https://httpbin.org/get")
-w <duration> Duration of wait seconds (e.g., 1s, 500ms, 2m) (default 1s)
どうでしょうか? Go 標準の flag パッケージだけにしては結構見やすくなっているのではないでしょうか。
興味があれば、ぜひ詳細を読んでみてください。
Custom Usage で見やすくしてるポイント
改めて
- flag デフォルトの Usage
- Custom Usage
の違いを見てみましょう。
Before: flag デフォルトの Usage
$ go run main.go -h
Usage of /var/folders/vv/fc_khb3904d2d0_rz86jxhv00000gn/T/go-build1709560788/b001/exe/main:
-d Debug mode
-f string
(required) File path
-l int
Index of line (default 10)
-u string
URL (default "https://httpbin.org/get")
-w duration
Duration of wait seconds (e.g., 1s, 500ms, 2m) (default 1s)
After: Custom Usage
$ go run main.go -h
Usage: main [OPTIONS]
Description:
A sample command demonstrating simple template usage in Go CLI applications.
Options:
-f <string> (required) File path
-d Debug mode
-l <int> Index of line (default 10)
-u <string> URL (default "https://httpbin.org/get")
-w <duration> Duration of wait seconds (e.g., 1s, 500ms, 2m) (default 1s)
かなり印象が違って見えるのではないでしょうか? では、Custom Usage で見やすくしているポイントを具体的に見ていきます。
正確な実行バイナリ名を表示する
$ go run main.go -h
Usage: main [OPTIONS]
...
通常 Go の flag パッケージを使うと、 Usage 出力時に Usage of {os.Args[0]} のような仕様で文字列が表示されます。
この os.Args[0] は実行したコマンドラインの最初の引数、つまり実行バイナリ名が通常入るのですが、厳密にいうと実行方法によっては実行されたバイナリ名にならないことがあります。
例えばシェルスクリプト等の中で exec -a "hoge" ./mycli ... のようにプロセス名を指定して呼び出した場合や、シンボリックリンク経由で実行した場合などです。
こういった場合でも正確な実行バイナリ名を表示するために、 Custom Usage では os.Executable() を使って実行バイナリのパスを取得し、 filepath.Base() でファイル名だけを抽出して表示しています。
これにより、どのような実行方法であっても正確な実行バイナリ名がUsageに表示されるようになります。
コマンドの説明文の表示
Description:
A sample command demonstrating simple template usage in Go CLI applications.
ここは好みの問題かもしれませんが、Descriptionがあるだけで一気に “ちゃんとしたツール感” が出るので、私はいつもコマンドのディスクリプションを表示するようにしてます。
オプション一覧が一行で表示される
今回紹介する Custom Usage の一番の売りがここなんですが、オプション一覧がライブラリを使用したかのように一行で綺麗に表示されるようになります。
Options:
-f <string> (required) File path
-d Debug mode
-l <int> Index of line (default 10)
-u <string> URL (default "https://httpbin.org/get")
-w <duration> Duration of wait seconds (e.g., 1s, 500ms, 2m) (default 1s)
型名を <string> のように括弧で括ってるのはフラグ名と値の区別がつきやすくするためで、他のコマンドでも見かけたことがあり、分かりやすかったので採用してます。
(簡単な実装なので、この辺りのフォーマットを好みに応じて変えたい場合はすぐ変えられます)
必須のオプションがどれか分かりやすい(上位に表示される)
Options:
-f <string> (required) File path ← 必須オプションが一番上に来る
-d Debug mode
-l <int> Index of line (default 10)
-u <string> URL (default "https://httpbin.org/get")
-w <duration> Duration of wait seconds (e.g., 1s, 500ms, 2m) (default 1s)
このオプションの並びに注目して頂きたいのですが、 -f オプションが一番上に来ています。
通常、 flag パッケージを使うと、オプションは英単語の昇順に表示されるため、必須オプションが上に来るとは限りません。
そうなると、Usageを見たときに必須オプションがどれなのかぱっと見分かりづらいです。
Custom Usage では、必須オプションに特定のマークを付与し、そのマークを元にソートを行うことで、必須オプションが上に来るようにしています。
これにより、Usageを見たときに必須オプションが強調されて一目で分かるようになります。
Custom Usage のスニペットを紹介
では、早速これらを実現するためのコードスニペットを紹介します! flagパッケージを使ったコマンドラインツールに、以下のコードを追加してください。
const (
// 必須パラメターがあればこれを使ってマークする(この印を使ってUsage出力時に必須パラメタが上に来るようにソートする処理が入ってます)
Req = "(required) "
)
var (
commandDescription = "A sample command demonstrating simple template usage in Go CLI applications."
// 以下に他のオプションを定義していく...
// optionFilePath = flag.String("f", "", Req+"File path")
// optionUrl = flag.String("u", "https://httpbin.org/get", "URL")
//...
)
func init() {
flag.Usage = customUsage(commandDescription)
}
func customUsage(description string) func() {
optionFieldWidth := "16" // Recommended width = general: 16, bool only: 5
b := new(bytes.Buffer)
func() { flag.CommandLine.SetOutput(b); flag.PrintDefaults(); flag.CommandLine.SetOutput(nil) }()
return func() {
re := regexp.MustCompile(`(?m)^ +(-\S+)(?: (\S+))?\n*(\s+)(.*)\n`)
usages := strings.Split(re.ReplaceAllStringFunc(b.String(), func(m string) string {
valueType := strings.ReplaceAll("<"+strings.TrimSpace(re.FindStringSubmatch(m)[2])+">", "<>", "")
return fmt.Sprintf(" %-"+optionFieldWidth+"s %s\n", re.FindStringSubmatch(m)[1]+" "+valueType, re.FindStringSubmatch(m)[4])
}), "\n")
sort.SliceStable(usages, func(i, j int) bool { return strings.Contains(usages[i], Req) && !strings.Contains(usages[j], Req) })
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [OPTIONS]\n\n", func() string { e, _ := os.Executable(); return filepath.Base(e) }())
fmt.Fprintf(flag.CommandLine.Output(), "Description:\n %s\n\n", description)
fmt.Fprintf(flag.CommandLine.Output(), "Options:\n%s", strings.Join(usages, "\n"))
}
}
func main() {
// コマンドのメイン処理
}
customUsage は結構ごちゃごちゃしてますが、やってることを簡単に説明すると、 flag パッケージの Usage 出力を変数に入れてしまい、正規表現で必要な情報を取り出してから整形してます。
このとき、必須オプションには Req 定数を付与し、そのマークを元にソートを行うことで必須オプションが上に来るようにしています。
あとは、 os.Executable() を使って実行バイナリ名を取得し、 Usage の最初の行に表示しています。
customUsageで正規表現でUsageをパースしている理由
flag パッケージに詳しい方なら
flag パッケージの
flag.VisitAllで登録されているフラグをすべて取得できるから、わざわざ正規表現で Usage をパースしなくてもいいのでは?
と思うかもしれません。確かに flag.VisitAll を使えば登録されているフラグをすべて取得できます。
ただ、 flag.VisitAll を使う場合一点大きな問題がありました。それは「デフォルト値がわからない」という点です。
flag パッケージにはフラグのデフォルト値を取得するための関数が用意されておらず、 flag.VisitAll で取得できるのは
- フラグ名
- 設定されている値(デフォルトかどうかのフラグなし)
- 説明文
の3つだけです。
このため、 flag.VisitAll を使って Usage を生成しようとすると、各フラグのデフォルト値が表示できなくなってしまいます。
flag パッケージはどうやってデフォルト値を判定しているのかというと…
flag.go - Go
https://cs.opensource.google/go/go/+/master:src/flag/flag.go;l=536-561?q=flag&ss=go%2Fgo
─
isZeroValue determines whether the string represents the zero
パッケージ内のprivate関数で判定する処理を実装しているんですよね。 ということで、これをそのまま丸々コピペするわけにはいかず、flag側が判定してくれたデフォルト値を Usage 出力からパースして再利用してる、という経緯でした。
必須オプションのチェックについて
flag パッケージには必須オプションを指定する機能がないため、必須オプションのチェックは自分で実装する必要があります。
例えば、以下のように flag.Parse() の後に必須オプションが設定されているかどうかをチェックするコードを追加します。
func main() {
flag.Parse()
if *optionFilePath == "" {
fmt.Printf("\n[ERROR] Missing required option\n\n")
flag.Usage()
os.Exit(1)
}
}
スニペットの説明
上記のスニペットでは、以下のポイントに注意してください。
Req定数は、必須オプションを示すためのマークです。flagを定義する際の説明文の先頭にくっつけてください。Req定数の中身の文字列自体は好みに応じて変更してください。
commandDescription変数には、コマンドの説明文を設定します。init関数でflag.UsageにcustomUsageを設定しています。- init関数はプログラムの実行前に自動的に呼び出されるため、ここで Usage を設定することで、プログラム全体でカスタム Usage が使用されます。
- もし Usage を表示した際にフラグと説明文の間のスペースが狭いと感じた場合は、
optionFieldWidth変数の値を調整してください。デフォルトは16に設定しています。
ヘルプ表示について
以前は -h や --help オプションを自分で実装してヘルプ表示を行っていたんですが、ある時 flag パッケージはデフォルトで -h オプションをサポートしていることに気づきました。
flag.go - Go
https://cs.opensource.google/go/go/+/master:src/flag/flag.go;l=1109-1116?q=flag&ss=go%2Fgo
─
if name == “help” || name == “h” { // special case for nice help message.
そのため、Custom Usage のスニペットでは特に -h や --help オプションを実装していませんが、問題なくヘルプ表示は動作します。
まとめ
Go 標準の flag パッケージを使ったコマンドラインアプリケーションで、 Usage を綺麗に整形して見やすくする方法について紹介しました。 Custom Usage を使うことで、外部ライブラリを使わずに見やすい Usage を実現できます。 ぜひ試してみてください!