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 さんに感謝 🙇

参考図書

photo
プログラミング言語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 現在)。復刊を望む!

reviewed by Spiegel on 2016-07-13 (powered by PA-APIv5)