YAMLファイルをCLI(Shell)で操作(編集)するライブラリの選定にご注意


結論:(綺麗な)できるだけ元ファイルに近いYAMLを得たいのであればruamel.yamlを使いましょう。

CSVにyamlのkeyとvalueが列挙されていて、これに従って特定のyamlファイルを機械的に更新したい!という要望がありました。

yamlをShell, Ruby, Pythonあたりで操作するツール・ライブラリを用意して、ちょっとしたスクリプトを書いて実行しよう!と思いました。

Case1. shellで使えるpandastrike/yaml-cli

https://github.com/pandastrike/yaml-cli

※2019/10現時点で4年メンテされておらず、PRも放置されています…

npmで公開されているので、インストールが簡単で、使い方も簡単です。READMEを見れば、すぐに使えるでしょう。

ですが、問題があります。

以下は実行例です。

% cat example.yml
 foo:
   common:
     msg1: &msg1 'hello'
   foo:
     msg1: *msg1
   bar:
     msg2: 'good morning'
%
%
% yaml set example.yml foo.bar.msg2 bye
 foo:
   common:
     msg1: hello
   foo:
     msg1: hello
   bar:
     msg2: bye

yamlにはanchor (&)とalias (*) という機能があります。ようはCのポインタと同じですね。参照できます。

ですが、yaml-cliにかけるとそれらが消えてしまっています。

TOP階層だけ維持される、というIssueもありますね。
https://github.com/pandastrike/yaml-cli/issues/26

この問題はpandastrike/yaml-cli単独ではおそらく解決しないでしょう。このライブラリは内部的にjs-yamlを使っている薄いラッパーなので。

Case2. node.js経由でnodeca/js-yamlを使う

内部的には(古い)js-yamlを使っているので、最新のjs-yamlはどうか?と思って使ってみました。

https://github.com/nodeca/js-yaml

% node
Welcome to Node.js v12.6.0.
Type ".help" for more information.

> const yaml = require('js-yaml');
undefined
> const fs = require('fs');
undefined
> const doc = yaml.safeLoad(fs.readFileSync('example.yml', 'utf8'))
undefined
> yaml.safeDump(doc)
'foo:\n' +
  '  common:\n' +
  '    msg1: hello\n' +
  '  foo:\n' +
  '    msg1: hello\n' +
  '  bar:\n' +
  '    msg2: good morning\n' 

alias置き換わってますね・・・というわけでNGです。

ちなみにjs-yamlというコマンドも用意されているのですが、読み込んだyamlをjsonで出力するだけで、値をセットしたりする機能はないし、使えなさそうです。

Case3. kislyuk/yq

https://github.com/kislyuk/yq

結構メンテされていて良いと思うのですが

% yq write example.yml foo.bar.msg2 bye
foo:
  common:
    msg1: hello
  foo:
    msg1: hello
  bar:
    msg2: bye

残念。これもダメですね。anchor消えています。

内部的にはPyYAMLに依存しているようです。後述の良いライブラリを見つけたのでPyYAMLは調べていません。

Case4. 要望を満たせる ruamel.yaml

「anchor alias keep YAML library」などで検索したところruamel.yamlが使えそうということが判明。

https://bitbucket.org/ruamel/yaml/src/default/

これはPythonライブラリです。Shellで使う場合は
https://bitbucket.org/ruamel/yaml.cmd/src/default/
こちらが用意されているのですが、特定のkeyの値を変更する、というようなことができないので、利用は断念しました。

以下はライブラリをpythonのREPL経由で使ってみた例です。

% pip install ruamel.yaml
% python
Python 2.7.15 (default, Oct  4 2018, 21:16:51)
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.10.44.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ruamel
>>> import ruamel.yaml
>>> yaml = ruamel.yaml.YAML()
>>> with open('example.yml') as stream:
...     data = yaml.load(stream)
...
>>> with open('out.yml', 'w') as stream:
...     yaml.dump(data, stream=stream)
...
>>> data
ordereddict([('foo', ordereddict([('common', ordereddict([('msg1', u'hello')])), ('foo', ordereddict([('msg1', u'hello')])), ('bar', ordereddict([('msg2', 'good morning')]))]))])
>>> exit()
% cat out.yml
foo:
  common:
    msg1: &msg1 hello
  foo:
    msg1: *msg1
  bar:
    msg2: good morning

quoteが消えてますが、自分は許容範囲です。anchorとaliasがちゃんと維持されてますね。素晴らしい!ちなみにコメントなども維持されるようです。
このあたりが参考になります。色々と実験されているようです。
https://dev.classmethod.jp/server-side/python/getting-started-with-pyyaml-and-ruamel-yaml/

上記のREPLでdataを出力させていますが、データ構造がordereddictですね。このあたりのデータ構造の選択・設計が他のライブラリとは違うのだと思います。(他のライブラリは内部的にjsonやhashmapで持っていたのでコメントやanchorが破棄される運命にあったのかと思います。データ構造大事)

しかしordereddictの中にanchor名が入っていないので謎ですね・・・ライブラリの中で状態を持っているんでしょうか。。。コード深く読んでないのでわからないです。詳しい方教えてください!

ちなみに、当初やりたかった値の設定はこうすればできました。
data[‘foo’][‘bar’][‘msg2’] = ‘bye’

以上です。

できればRuby scriptから呼び出したかったのでShellで使えるコマンドが望ましかったのですが、Pythonでも問題ないので、Pythonで当初の問題を扱うことにしました。

ありがとう!ruamel.yaml