CLI作成用パッケージ「urfave/cli」を用いた際に設定ファイルを用いてオプションの初期値をカスタマイズする

[「urfave/cli」](https://github.com/urfave/cli)を用いて[「hlt」](https://github.com/x-color/hlt)というCLIを作成した際に、設定ファイルを用いてコマンドのオプションの初期値をカスタマイズする方法を調べたので書いておく。

# 何がしたかったのか

CLIのオプションの初期値を設定ファイルを用いて外部から設定可能にしたい。

```yaml:config.yaml
number: 9
word: message
```

上記のような設定ファイルを用意した場合、以下を実行しただけで、`number = 9`, `word = "message"`となって欲しい。

```bash
# CLIを実行
$ cli
# number = 9
# word = "message"
```

# 解決策

「urfave/cli」のREADMEにも書いてあるが、[「urfave/cli/altsrc」](https://github.com/urfave/cli/tree/master/altsrc)を用いるとできる。
しかし、READMEに書いてある[サンプルコード](https://github.com/urfave/cli#values-from-alternate-input-sources-yaml-toml-and-others)は、オプションによって設定ファイルを指定して動作するものになっていたので、今回のように最初から設定ファイルが決まっている場合向けではなかった。
また、サンプルコードをそのままコピペして実行してみても、型の不一致で実行できなかった...

それらの解決方法を説明する前に、先に解決した際のコードを載せておく。

```go
package main

import (
	"fmt"
	"log"
	"os"

	"gopkg.in/urfave/cli.v1" // "github.com/urfave/cli"ではうまくいかない
    "github.com/urfave/cli/altsrc" // 設定ファイルを用いるためのパッケージ
)

func main() {
	app := cli.NewApp()

    // オプションの用意。初期値をファイルで決めるオプションは`altsrc.NewXXXFlag()`を用いて定義する
	flags := []cli.Flag{
		altsrc.NewIntFlag(cli.IntFlag{Name: "number"}),
		altsrc.NewStringFlag(cli.StringFlag{Name: "word"}),
	}

	app.Action = func(c *cli.Context) error {
		fmt.Println("number:", c.Int("number"))
		fmt.Println("word:", c.String("word"))
		return nil
	}

    // 実行時にファイルを読み込み、オプションの初期値を設定させる
	app.Before = altsrc.InitInputSource(flags, func() (altsrc.InputSourceContext, error) {
		return altsrc.NewYamlSourceFromFile("config.yaml")
	})
	app.Flags = flags

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}
```

## 決まった設定ファイルを読み込む

サンプルコードでは以下のコードが用いられており、これは、`load`オプションに設定されたファイルから読み込むプログラムとなっている。

```go
altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load"))
```

ファイルを直接指定して読み込むには、以下のコードを用いると良い。

```go
altsrc.NewYamlSourceFromFile("config.yaml")
```

これならば、`config.yaml`を直接読み込ませることができる。

しかし、`altsrc.NewYamlSourceFromFlagFunc()`を`altsrc.NewYamlSourceFromFile()`にしただけでは、うまくいかない。前者は返り値が関数なのに対して、後者は関数ではないため、単純に型が合わない。

なので、`altsrc.InitInputSourceWithContext()`の代わりに`altsrc.InitInputSource()`を用い、さらに、`altsrc.NewYamlSourceFromFile()`を無名関数でラップする。

ラップする理由は、`altsrc.InitInputSource()`の第二引数が`func() (altsrc.InputSourceContext, error)`を要求しているのに対して、`altsrc.NewYamlSourceFromFile()`が`altsrc.InputSourceContext, error`を返すためである。ラップすることで型を合わせている。

```go
altsrc.InitInputSource(flags, func() (altsrc.InputSourceContext, error) {
    return altsrc.NewYamlSourceFromFile("config.yaml")
})
```

後は、これを`app.Before`などに渡すことで自動で実行してくれる。
これによってようやく、決まった設定ファイルを読み込むことができるようになる。

## 型エラーを回避する

もう一つの問題点として、サンプルコードを実行してみても型エラーが発生してうまく動かない。
この問題は、「urfave/cli/altsrc」のコードの`import`部分を見てみると原因がすぐわかる。

サンプルコードの`import`部分は以下のようになっている。

```go
import (
    "github.com/urfave/cli"
    "github.com/urfave/cli/altsrc"
)
```

これに対して、`github.com/urfave/cli/altsrc`の`import`部分は、

```go
import (
    "gopkg.in/urfave/cli.v1"
)
```

となっていて、`altsrc`の関数が返す`cli.XXX`の型は`gopkg.in/urfave/cli.v1`を参照して定義されているのに対して、サンプルコード内の`cli.XXX`型は`github.com/urfave/cli`を参照して定義されている。
なので、実行時に違う型として判定されてしまい、型エラーが発生している。

これを回避するには、サンプルコードの`import`部分を以下のようにすれば良い。

```go
import (
    "github.com/urfave/cli/altsrc"
    "gopkg.in/urfave/cli.v1"
)
```

# おわりに

せっかく調べたけども、このパッケージではやりたいことがうまくできなかったので、結局欲しい機能だけ自分で実装する羽目になってしまった...