struct2flag でコマンドライン解析
いきなりだが,以下のコードから始める。
package main import ( "flag" "fmt" "strings" ) var n = flag.Bool("n", false, "omit trailing newline") var sep = flag.String("s", " ", "separator") func main() { flag.Parse() fmt.Print(strings.Join(flag.Args(), *sep)) if !*n { fmt.Println() } }
これはコマンドラインの文字列をそのまま出力するプログラムで,こんなふうに動作する。
$ go run echo4.go May the Force be with you
May the Force be with you
-s
オプションで区切り文字を指定できる。
こんな感じ。
$ go run echo4.go -s / May the Force be with you
May/the/Force/be/with/you
また -n
オプションを指定すると,末尾の改行が抑制される。
さらに -h
オプションでヘルプが表示される。
$ go run echo4.go -h
Usage of /home/spiegel/.cache/go-build/06/06a6e71bb093bd1ebbb176c5042329730592597ae86dcb2ca99b3759e1aecb18-d/echo4:
-n omit trailing newline
-s string
separator (default " ")
これらオプションの制御を行っているのが標準パッケージ flag
である。
上述のコード例は簡単なのでアレでいいのだが,オプションに紐付けられた変数をひとつずつ定義して取り回しするのは面倒なので,普通は構造体にまとめる。
例えばこんな感じ。
package main
import (
"flag"
"fmt"
"strings"
)
type Flags struct {
N bool
Sep string
}
func (f *Flags) Bind() {
flag.BoolVar(&f.N, "n", false, "omit trailing newline")
flag.StringVar(&f.Sep, "s", " ", "separator")
}
func main() {
f := &Flags{}
f.Bind()
flag.Parse()
fmt.Print(strings.Join(flag.Args(), f.Sep))
if !f.N {
fmt.Println()
}
}
オプションを構造体にまとめるメリットはもうひとつあって,それは facade パターンを構成してコマンドライン制御とロジックを簡単に分離できることである。 例えばこんな感じ。
package main
import (
"flag"
"fmt"
"strings"
)
type Flags struct {
N bool
Sep string
Strs []string
}
func (f *Flags) Bind() {
flag.BoolVar(&f.N, "n", false, "omit trailing newline")
flag.StringVar(&f.Sep, "s", " ", "separator")
}
func Echo(f *Flags) {
fmt.Print(strings.Join(f.Strs, f.Sep))
if !f.N {
fmt.Println()
}
}
func main() {
f := &Flags{}
f.Bind()
flag.Parse()
f.Strs = flag.Args()
Echo(f)
}
これで main()
関数はコマンドライン制御に徹してロジックを Echo()
関数に任せることができる。
一方で Echo()
関数は渡される Flags
構造体にのみ依存しているのでコマンドライン制御の詳細を知らなくて済む。
この例では全てが main
パッケージにまとまってるので分かりにくいが,ロジックを別パッケージにすることはよくあるので,テストのしやすさも考慮してこのパターンを意識するのは大事である。
とはいえ,ロジックが変われば要求される情報も変わるし,上の例でいう Flags
構造体の中身も変わる。
そうなると Flags.Bind()
メソッドの中身も変える必要があるが,このメソッドで構造体の要素をひとつづつ flag
パッケージに紐付けているので,これを書くのが地味に面倒くさいのである。
そもそも flag
パッケージに紐付ける処理が構造体側にあるのもイマイチだよね。
というところで github.com/hymkor/struct2flag
パッケージの登場である。
これを使ってコードを書き直してみる。
package main
import (
"flag"
"fmt"
"strings"
"github.com/hymkor/struct2flag"
)
type Flags struct {
N bool `flag:"n,omit trailing newline"`
Sep string `flag:"s,separator"`
Strs []string
}
func NewFlags() *Flags {
return &Flags{N: false, Sep: " ", Strs: []string{}}
}
func Echo(f *Flags) {
fmt.Print(strings.Join(f.Strs, f.Sep))
if !f.N {
fmt.Println()
}
}
func main() {
f := NewFlags()
struct2flag.BindDefault(f)
flag.Parse()
f.Strs = flag.Args()
Echo(f)
}
struct2flag
を使うことにより flag
との紐づけ情報は Flags
構造体定義のタグとインスタンス生成・初期化を行う NewFlags()
関数に集約される(インスタンスの値が各オプションの既定値として登録されるため)。
struct2flag
を使うメリットはいくつかあって
main()
関数側はオプション値を格納する構造体の詳細を知らなくてもよい- 構造体のタグが各要素の簡単な説明になっている
struct2flag
の中身がシンプルで標準以外の依存パッケージがない
といったところであろう。
私は,ちゃんとしたコマンドラインツールを作るときはサブコマンドや GNU 拡張シンタックスが使える cobra
を使ったりしているのだが,ちょっとしたツールには大げさなんだよね,これ。
かといって flag
を素のまま使うのは微妙に鬱陶しくて,結局コマンドラインオプション無しで main()
関数に直接値を埋め込んでしまうことが多い。
struct2flag
なら構造体にタグを書くだけで済むし flag
を素のまま使うよりは気軽にコマンドラインオプションを構成できるので,ちょっとしたツールにはいい感じである。
公開してくださった @zetamatta@mstdn.jp さんに感謝 🙇
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!