YAMLファイルの全ての値にos.Expandenv(os.Expand)を適用するgithub.com/k1LoW/expandを作った

私は作るツールの設定ファイルのフォーマットをYAMLにすることが多いです。

そして各値で環境変数を展開できるようにする機能を追加することがあります。

以下のような設定ファイルを読み込んだ際に、 ${COVERAGE_ACCEPTABLE}${COVERAGE_BADGE_PATH}環境変数から読み込んで変数展開してあげる機能を追加します。

coverage:
  acceptable: ${COVERAGE_ACCEPTABLE}
  badge:
    path: ${COVERAGE_BADGE_PATH}
comment:
  enable: ${COMMENT_ENABLE}

値が文字列であればYAMLファイルをエンコードしてから os.Expandenv を呼べばいいですし、今まではそのように作っていたのですが以下のような課題がありました。

数値や真偽値の変数展開の実装が面倒

${xxxxx} はどう見ても文字列なので、数値や真偽値の変数展開を実現するためにはUnmarshalする際に一工夫が必要になります。

そこまでの実装のモチベーションが持てず、大抵「環境変数展開対象外」にしていました。

os.Expandenv をひたすら書く必要がある

YAMLの文字列を構造体に変更した後に変数展開をする場合、os.Expandenvを各値に対してひたすら適用する必要があります*1。適用漏れもあったり。

そういったYAMLを設定ファイルに採用しているツールが手元に増えてきたので、YAMLファイルの全ての値に対してos.Expandenv(os.Expand)を適用する関数を作ってみました。

github.com

github.com/k1LoW/expand

使い方は簡単で、以下のように expand.Expand* を呼べば []byte や string なYAMLの各値にだけos.Expandenv(os.Expand)を適用します。

c := &Config{}
p := "config.yml"
buf, err := os.ReadFile(p)
if err != nil {
    return err
}
if err := yaml.Unmarshal(expand.ExpandenvYAMLBytes(buf), c); err != nil {
    return err
}

提供している関数は expand package - github.com/k1LoW/expand - pkg.go.dev をみてください。

実装は、ありがたいことに github.com/goccy/go-yaml が Lexer をpublicなAPIとして公開してくれているので、それを利用させてもらっています。 Lexer の活用事例として ycat があり、かなり参考にさせてもらいました*2

github.com

今後

せっかくYAMLのための関数を作ったので、他のフォーマットや構造体の関数も作ってみようかなとは思いました。思っただけです。

もし何かあればフィードバックもらえると嬉しいです。

*1:全ての値を走査するために「reflectを使って再帰的に」という手段もあるとは思います

*2:特に func (p *Printer) PrintTokens(tokens token.Tokens) string。ほぼそのままといってもいいです