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
結構メンテされていて良いと思うのですが
% 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