codegangsta/cli の YAML ファイルの読み込み機能を試してみた

[https://github.com/codegangsta/cli](https://github.com/codegangsta/cli) のオプションを YAML ファイルからロードする機能がついたようなので試してみたのでメモします。

パッケージを準備する
--------

いきなりですが、[YAMLファイルの読み込みが実装されたコミット](https://github.com/codegangsta/cli/commit/6bb6787f337945abc3f8d92147bb8c04692c3e0e)そのもののリビジョンを利用しても動きません。それに対して[自分が行ったコントリビューション](https://github.com/codegangsta/cli/commit/a7936f1d21fd8272ce215f3710a799960427c79c)があるのですが、これより後のリビジョンが利用されるようにしてください（最新バージョンを利用すれば良いと思います）。

codegangsta/cli/altsrc パッケージ
--------

まず、完全に動作するサンプルを [https://github.com/ykanda/cli-example](https://github.com/ykanda/cli-example) に用意しましたので、省略されていないコードを見たいかた、実際に動作させたい方は、こちらを見てください。

さて、YAML ファイルのロード機能は、codegangsta/cli のサブパッケージ codegangsta/cli/altsrc として実装されています。これを元々の codegangsta/cli と組み合わせて使うことで、オプションで読み込みファイルを指定したり、ファイルから入力できるオプションを作ることができます。
cli.App のオプションフラグについて、YAML ファイルのロードをサポートするための簡単なサンプルコードは次のようになります。

```go
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` というファイルをロードするようになっています。このファイルの中身は、次のようなものとします。

```yaml
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()` といった種類があり、目的とする値の型によって使い分けます。

```go
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()` というユーティリティ関数です。次のような使い方をします。

```go
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` に対する場合と代わりませんので、いきなりコードからみてみましょう。

```go
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 と同じように設定していると考えれば、さほどむずかしいことはないでしょう。設定ファイルは次のようなものとしてみましょう。

```yaml
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](https://github.com/ykanda/cli-example) に用意しましたので、そちらをごらんください。


暗黙的にホームディレクトリ以下にあるファイルを読むようにする
--------

`--load` オプションに設定するデフォルト値によって、暗黙的にユーザのホームディレクトリ直下にあるファイルを読むようにしたい、という場合は [https://github.com/codegangsta/cli/pull/348#issuecomment-208034424](https://github.com/codegangsta/cli/pull/348#issuecomment-208034424) に示されているように `os/user` パッケージを用いると良いようです。

```go
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"),
)
```