前置き
- 社内勉強会のLTで発表したら好評だったので投下
- 自称YAMLエンジニアのsue445が今まで踏んだ罠をクイズにしました
- Ruby 3.0.0の Psych で動作確認していますが他言語での挙動は調べていません
- Psychがlibyamlベースなので他の言語のパーサでもだいたい同じ挙動をすると思うけど
練習問題
Q: 出力されるものは?
yaml = <<YAML
a: 1
YAML
YAML.load(yaml)
{"a"=>"1"}
{"a"=>1}
- シンタックスエラー
- その他
回答
2
YAMLの数字っぽい文字列はその言語の数字の型(Rubyだと Integer
)として解釈されます。
文字列として解釈させたい場合は "1"
(ダブルクオーテーション)や '1'
(シングルクォーテーション)のように囲んでください
問題1
Q: 出力されるものは?
yaml = <<YAML
splash: ふたりはプリキュア Splash Star
yes: Yes!プリキュア5
yes_gogo: Yes!プリキュア5GoGo
fresh: フレッシュプリキュア!
YAML
data = YAML.load(yaml)
"#{data['splash']},#{data['yes']},#{data['yes_gogo']},#{data['fresh']}"
"ふたりはプリキュア Splash Star,Yes!プリキュア5,Yes!プリキュア5GoGo,フレッシュプリキュア!"
"ふたりはプリキュア Splash Star,,Yes!プリキュア5GoGo,フレッシュプリキュア!"
",Yes!プリキュア5,Yes!プリキュア5GoGo,フレッシュプリキュア!"
- シンタックスエラー
回答
2
true
, false
, yes
, no
, on
, off
はダブルクオーテーションなどで囲まない限りYAMLでは全て 真偽値 として扱われます
data = YAML.load(yaml)
=> {"splash"=>"ふたりはプリキュア Splash Star", true=>"Yes!プリキュア5GoGo!", "yes_gogo"=>"Yes!プリキュア5GoGo", "fresh"=>"フレッシュプリキュア!"}
data = YAML.load("{1: yes, 2: Yes, 3: on, 4: On, 5: true, 6: True}")
data = YAML.load("{1: no, 2: No, 3: off, 4: Off, 5: false, 6: False}")
問題2
Q: 出力されるものは?
yaml = <<YAML
default: &default
slack:
webhook_url: "https://example.com/"
channel: "random"
production:
<<: *default
slack:
channel: "production_notify"
YAML
data = YAML.load(yaml)
data["production"]
{"slack"=>{"webhook_url"=>"https://example.com/", "channel"=>"random"}}
{"slack"=>{"webhook_url"=>"https://example.com/", "channel"=>"production_notify"}}
{"slack"=>{"channel"=>"production_notify"}}
- シンタックスエラー
回答
3
&
(アンカー)と *
(エイリアス)で定義済みの値をいい感じに共通化できるのはYAMLのよくあるリファクタリング手法ですが、<<:
(マージ)はdeep merge(要素内に別の要素があった時に再帰的にマージされる)ではなく第1要素だけを上書きするマージ(代入に近い)なので、今回の場合 default
の内容が打ち消されます。
イメージ的にはこんな感じ
data["slack"] = {"webhook_url"=>"https://example.com/", "channel"=>"random"}
data["slack"] = {"channel"=>"production_notify"}
余談ですが https://github.com/railsware/global はdeep mergeしてくれるのがかなり便利で、前職のRailsアプリにはだいたい入っていました
問題3
Q: 出力されるものは?
yaml = <<YAML
go:
- 1.9
- 1.10
- 1.11
- 1.12
YAML
data = YAML.load(yaml)
data["go"]
[1.9, 1.10, 1.11, 1.12]
[1.9, 1.1, 1.11, 1.12]
[1.90, 1.10, 1.11, 1.12]
- シンタックスエラー
回答
2
クオーテーションで囲んでいないので小数として解釈されるため 1.10
は 1.1
として解釈されます
厳密に 1.10
として評価するには "1.10"
のように囲む必要があります。
余談ですがGo 1.10が出た時に .travis.yml
に 1.10
を追加したらGo 1.1でCIが実行されてビルドが失敗したことがあります。
https://github.com/sue445/zatsu_monitor/commit/fc6b8ab806a3c48617cb084437d2d1c018777ee9#comments
問題4
Q: この中でシンタックスエラーになるのはどれか?
a
excludes:
- *_test.rb
b
excludes:
- test/**
- aのみシンタックスエラー
- bのみシンタックスエラー
- aとb両方シンタックスエラー
- シンタックスエラーは無い
回答
1
a
yaml = <<YAML
excludes:
- *_test.rb
YAML
YAML.load(yaml)
Traceback (most recent call last):
8: from /Users/sue445/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
7: from /Users/sue445/.rbenv/versions/3.0.0/bin/irb:23:in `load'
6: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
5: from (irb):62:in `<main>'
4: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:280:in `load'
3: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:390:in `parse'
2: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:456:in `parse_stream'
1: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:456:in `parse'
Psych::SyntaxError ((<unknown>): did not find expected alphabetic or numeric character while scanning an alias at line 2 column 3)
b
yaml = <<YAML
excludes:
- test/**
YAML
YAML.load(yaml)
*_test.rb
の *
が前述のエイリアスとして評価されるのですが、対応する _test.rb
という名前のアンカーが存在しないためシンタックスエラーになります。
シンタックスエラーにしないためには "*_test.rb"
のように囲む必要があります。
参考文献
おまけ:LT直後のみんなの反応
- 「囲んでも囲まなくてもいいみたいなsyntaxやだ…」
- 「問題として聞かれるとわかるけど、実際に yaml 書いてたらミスりそう」
- 「YAMLの特別な挙動にうごかされないようにするのは、ダブルクォートつけるのがいいのかな。」
- -> sue「『それはそう』なんですが、実際にそれやろうとするとかなり冗長になってだいぶ見づらくなるんですよね」
- 「
6E-1
が(文字列として判定されてほしいのに) 指数表記で判定されてハマった」
- 「YAMLほんとうにわかってなかった」