YAML の細かい仕様の話
グワーッ!
YAML 1.2 って、 yes,no,on,off 系が Boolean リテラルではなくなる破壊的変更あるん!?
Docker Compose v2.17.0 で内部パーサが go-yaml/yaml.v3 に更新され、真偽値を yes, no で書いてた compose.yml ファイルが軒並み must be a boolean エラー出まくってて死。https://t.co/PVpdIWFVgA— ながいの as a サービス (@longer_n) April 12, 2023
趣味アカウントのほうでバズった… ってほどでは無いけど、割と反響があったので、 YAML に関する話をいくつか。
YAML 1.2 では Boolean として扱うのは true, false だけ
https://yaml.org/type/bool.html
YAML 1.1 では 以下の文字が Boolean として認識されるのが一般的だ。
y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF
厳密には、 !!bool
タグを使って Boolean 型を表現する場合の表記方法だが、基本的には多くのパーサーではタグを書かなくても Boolean 型として扱われる。
https://yaml.org/spec/1.2.0/#id2602744
しかし YAML 1.2 以降ではこれが、 true
, false
のみが Boolean として認識されるという、だいぶデカい破壊的変更が入っている。
ここで、 1.2.0 の主たる更新点を見てみると…
https://yaml.org/spec/1.2.2/ext/changes/#changes-in-version-12-revision-120-2009-07-21
Changes in version 1.2 (revision 1.2.0) (2009-07-21)
[...]
- Only true and false strings are parsed as booleans (including True and TRUE); y, yes, on, and their negative counterparts are parsed as strings.
- Underlines _ cannot be used within numerical values.
- Octal values need a 0o prefix; e.g. 010 is now parsed with the value 10 rather than 8.
- The binary and sexagesimal integer formats have been dropped.
- The !!pairs, !!omap, !!set, !!timestamp and !!binary types have been dropped.
- The merge << and value = special mapping keys have been removed.
参考日本語訳:
- true と false の文字列のみがブーリアン(TrueとTRUEを含む)として解析される。 y, yes, on およびそれらの否定語は文字列として解析される。
- アンダーライン _ は、数値の中では使用できません。
- 8進数の値には0oのプレフィックスが必要です。例えば010は8ではなく10という値で解析されるようになりました。
- 二進法と十進法の整数形式は廃止されました。
- ペア、オマップ、セット、タイムスタンプ、バイナリ型は削除されました。
- merge << と value = の特殊マッピングキーは削除されました。
うーん、現代の セマンティック バージョニング だったら、絶対にマイナー場ジョンアップじゃダメなヤツ。
こういった仕様のため、 yes
, no
と書いた際にどう扱われるかは、パーサーの実装による。
例えば、 PyYAML などは Boolean リテラルとして扱われるが、 go-yaml/yaml.v3 では文字列として扱われる。
内部パーサーの更新でこの解釈が変わってしまったために発生したのが、冒頭の問題だ。
Ansible などでは、真偽値を yes, no と書く慣習があるせいで、もうめちゃくちゃあっちこっちで yes
, no
って書いてるわ…
なお、冒頭の Docker Compose については、 Docker Compose version v2.17.3 で yes
等を書いてもエラーにせずワーニングだけ出すように修正された。
[BUG] Error "must be a boolean" with yes
scalar in Docker Compose v2.17.0 · Issue #10465 · docker/compose
">" は全ての改行をスペースに置き換えるわけではない
ブロックスタイルの文字列表記で、 ">"
インジゲータを置くと、次の行から途中の改行が半角スペースに置き換えられると説明されがちだ。
foo: >
trimmed
line
しかし、半角スペースに置き換えられる条件はもう少し複雑だ。
https://yaml.org/spec/1.2.2/#line-folding
この Line Folding と言うのがこれに当たるのだが、これは長い行を折りたたんで読みやすくするための機能だ。
折りたたまれた部分(改行)が半角スペース (0x20) に変換されるものだが、
- 複数の改行が続く場合は最初の改行が破棄され、残りは保持される
- 先頭の空白を含むテキスト行(つまりインデントされた行)は、行の折り返しは適用されない。
という仕様がある。
このため、以下のように動作する。
YAML 入力:
foo: >
trimmed
line
breaking
but blank line and
nested
line
are are retained.
JSON 出力:
{ "foo": "trimmed line breaking\nbut blank line and\n nested\n line\nare are retained.\n" }
つまり、「改行を消す」というよりは、「次の行のインデントの高さがブロックの先頭と同じ場合のみ、行の折りたたみと見なして改行を削除する」という動作となる。
マージキー (<<) によるマージは 1回 しか記述できない。
複数のマップをマージしたい場合は、 シーケンス(配列)によって指定する。
https://yaml.org/type/merge.html
YAML 入力:
-
- &ANCHER1 { '0': 0, a: 0, b: 0 }
- &ANCHER2 { b: 99, c: 99 }
- &ANCHER3 { c: 255, d: 255 }
#- # Eror
# '0': 1
# << : *ANCHER1
# << : *ANCHER2
# << : *ANCHER3
# a: 1
- # Single map
b: 1
<< : *ANCHER2
- # Override multiple maps
'0': 1
<< :
- *ANCHER1
- *ANCHER2
- *ANCHER3
a: 1
JSON 出力:
[
[
{ "a": 0, "b": 0 },
{ "b": 99, "c": 99 },
{ "c": 255, "d": 255 }
],
{ "b": 1, "c": 99 },
{ "0": 1, "a": 1, "b": 0, "c": 99, "d": 255 }
]
シーケンスで複数のマップを指定した場合で、キーが重複している物は、シーケンス内では順序が前のものが常に優先される。
一方で、マージキーで指定したモノよりは、マージ先のものが優先となる。
ただ、パーサーによっては マージキー (<<
) を複数書く誤った書き方に対応しているモノがあり、
実際、 Docker Compose v2.17.0 で、内部パーサーが go-yaml/v3
に切り替わったときに、誤った書き方が対応から未対応に変わって、以下のようなエラーが出てハマった。
mapping key "<<" already defined at line nnn
もちろんこれは、マージキーを複数書く方が悪いので、 YAML の方を直す必要がある。
そして、もっとも恐ろしいことは、この機能も YAML 1.2 で削除されていることだ。
さすがに、主要な YAML 1.2 対応パーサーでも、マージ機能の削除まで行ったものは少ないが…