https://github.com/codegangsta/cli のオプションを YAML ファイルからロードする機能がついたようなので試してみたのでメモします。
パッケージを準備する
いきなりですが、YAMLファイルの読み込みが実装されたコミットそのもののリビジョンを利用しても動きません。それに対して自分が行ったコントリビューションがあるのですが、これより後のリビジョンが利用されるようにしてください(最新バージョンを利用すれば良いと思います)。
codegangsta/cli/altsrc パッケージ
まず、完全に動作するサンプルを https://github.com/ykanda/cli-example に用意しましたので、省略されていないコードを見たいかた、実際に動作させたい方は、こちらを見てください。
さて、YAML ファイルのロード機能は、codegangsta/cli のサブパッケージ codegangsta/cli/altsrc として実装されています。これを元々の codegangsta/cli と組み合わせて使うことで、オプションで読み込みファイルを指定したり、ファイルから入力できるオプションを作ることができます。
cli.App のオプションフラグについて、YAML ファイルのロードをサポートするための簡単なサンプルコードは次のようになります。
package main
import "fmt"
import "os"
import "github.com/codegangsta/cli"
import "github.com/codegangsta/cli/altsrc"
func main() {
app := cli.NewApp()
app.Flags = []cli.Flag{
altsrc.NewStringFlag(
cli.StringFlag{
Name: "test",
Value: "test default value",
},
),
cli.StringFlag{
Name: "load",
Value: "./.rc1",
},
}
app.Action = func(ctx *cli.Context) {
opt := ctx.String("test")
fmt.Println("cli-example --test =", opt)
}
app.Before = altsrc.InitInputSourceWithContext(
app.Flags,
altsrc.NewYamlSourceFromFlagFunc("load"),
)
app.Run(os.Args)
}
このコードは、カレントディレクトリにある .rc1
というファイルをロードするようになっています。このファイルの中身は、次のようなものとします。
test: "test by .rc1"
このコードをビルドして実行します。
オプションを指定しない場合、ファイルから読み込んだ値が利用されます。
読み込む元なるファイルは、--load
オプションによって指定される値が利用されていて、
メインコマンドの --load
オプションは、デフォルト値として "./.rc1 "
を持っていて、
特別に指定しなくとも .rc1
ファイルが利用されるというわけです。
$ ./cli-example
cli-example --test = test by .rc1
--test
オプションを指定することで、オプションをコマンドラインから指定することもできます。こうして指定されたオプションは、ファイルからの入力より優先されます。
$ ./cli-example --test "foo"
cli-example --test = foo
オプションを指定するキーを持たないファイルを読むと、デフォルト値が使われます。
ためしに、/dev/null
あたりを読んでみると次のようになります。
$ ./cli-example --load /dev/null
cli-example --test = test default value
少し詳しい解説
altsrc を利用するときのポイントは次のとおりです。
- App.Flags に指定する []cli.Flag の要素を alstc.New*Flag で生成する
- Before フックを altsrc.InitInputSourceWithContext で生成する
altsrc.New*Flag() によるオプション生成
ファイル入力によって指定したいオプションは、altsrc.New*Flag()
系の関数を使います。
これには NewIntFlag()
であるとか NewStringFlag()
といった種類があり、目的とする値の型によって使い分けます。
app := cli.NewApp()
app.Flags = []cli.Flag{
altsrc.NewStringFlag( // ファイル入力するオプションを生成
cli.StringFlag{
Name: "test",
Value: "test default value",
},
),
}
Before フックを altsrc.InitInputSourceWithContext で生成する
cli.App には Before というフィールドがあり、これにはオプションが解析される前段階における処理を行うフック関数を指定することができます。cli/altsrc によるファイルからのオプション読み込みはこれを利用して行います。自前でやろうとしたら、ファイルの入力、YAMLのパース、一致するオプションを探してその値を設定する、といったことを行わなければならないことは想像できると思います。
これらの処理を行う関数を生成することができるのが、altsrc.InitInputSourceWithContext()
というユーティリティ関数です。次のような使い方をします。
app := cli.NewApp()
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "load",
Value: "./.rc1",
},
}
app.Before = altsrc.InitInputSourceWithContext(
app.Flags,
altsrc.NewYamlSourceFromFlagFunc("load"),
)
上記の例では、第一引数に []cli.Flag
、第二引数に cli.NewYamlSourceFromFlagFunc("load")
という関数の戻り値を取っています。この辺がすこしややこしいですが、--load
というフラグの値を使ってファイルを読むようにする、という意味があるという形で理解しておけばよいでしょう。
サブコマンドのオプションをファイルから読み込む
codegangsta/cli にはサブコマンドを作る機能があり、これに対してもファイルからオプションを読み込む機能を提供することができます。基本的には cli.App
に対する場合と代わりませんので、いきなりコードからみてみましょう。
subCommand := cli.Command{}
subCommand.Name = "sub"
subCommand.Action = func(ctx *cli.Context) {
opt := ctx.String("test-sub")
fmt.Println("cli-example sub --test-sub =", opt)
}
subCommand.Flags = []cli.Flag{
altsrc.NewStringFlag(
cli.StringFlag{
Name: "test-sub",
Value: "test-sub default value",
},
),
cli.StringFlag{
Name: "load",
Value: "./.rc2",
},
}
subCommand.Before = altsrc.InitInputSourceWithContext(
subCommand.Flags,
altsrc.NewYamlSourceFromFlagFunc("load"),
)
app := cli.NewApp()
app.Commands = []cli.Command{
subCommand,
}
app.Run(os.Args)
単に、cli.App.Command の要素となる cli.Command に対して cli.App と同じように設定していると考えれば、さほどむずかしいことはないでしょう。設定ファイルは次のようなものとしてみましょう。
test-sub: "test-sub by .rc2"
実行結果は次のようになります。サブコマンドを与えると、サブコマンドのアクションが実行されることになりますが、そのとき .rc2
というファイルから入力された値が使われます。オプションを指定するキーを持たないファイルを渡したときや、オプションを明示的に渡したときの動作も全く同様となります。
$ ./cli-example sub
cli-example sub --test-sub = test-sub by .rc2
サブサブコマンドのオプションをファイルから読み込む
cli.Command はさらに Subcommand というフィールドを持ち、サブコマンドのサブコマンド、たとえば git remote add
などのような形のコマンドを作ることができます。Subcommands の型は実に []cli.Command
であるため、まったく同じような形とすることで、同じようにファイルからオプションをロードすることができます。ここでは詳細な説明は省きますが、冒頭で述べたとおり、完全に動作するサンプルを https://github.com/ykanda/cli-example に用意しましたので、そちらをごらんください。
暗黙的にホームディレクトリ以下にあるファイルを読むようにする
--load
オプションに設定するデフォルト値によって、暗黙的にユーザのホームディレクトリ直下にあるファイルを読むようにしたい、という場合は https://github.com/codegangsta/cli/pull/348#issuecomment-208034424 に示されているように os/user
パッケージを用いると良いようです。
u, err := user.Current()
// do something if err != nil
subCommand := new(cli.Command)
subCommand.Flags = []cli.Flag{
cli.StringFlag{
Name: "load",
Value: filepath.Join(user.HomeDir, ".mysrc"), // default value
},
}
subCommand.Before = altsrc.InitInputSourceWithContext(
subCommandCommand.Flags,
altsrc.NewYamlSourceFromFlagFunc("load"),
)