ぶていのログでぶログ

思い出したが吉日

RubyのYAML.dumpは改行の前にスペースを置くとエスケープされる

RubyのオブジェクトをYAML.dumpしてYAMLの設定ファイルを作るというコードを書いているときに、掲題の挙動に気がついた。 通常、複数行が含まれる文字列をYAML.dumpすると |- を使った表記になる。

$ ruby -ryaml -e 'puts YAML.dump("foo" => "a\nb")'
---
foo: |-
  a
  b

しかしながら改行の前にスペースを置くと |- は使われずダブルクォートで囲われた表記になった。

$ ruby -ryaml -e 'puts YAML.dump("foo" => "a \nb")'
---
foo: "a \nb"

どちらの場合もYAMLとしてvalidなのだけど*1、ダブルクォートで囲われている場合ダンプした文字列が長すぎて改行された状態になることがあり*2、その状態のYAMLをうまくパースできない処理系があるっぽい?*3が詳しくは未検証です🙏

Ruby以外の言語ではどうなのか?

というのが気になったのでPython、Node.js、Rust、Goで調べてみた。

Python

Python 3.11.3を使用した。

$ python
Python 3.11.3 (main, Apr  7 2023, 11:00:05) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import yaml
>>> print(yaml.dump({"foo": "a \nb"}))
foo: "a \nb"

>>> print(yaml.dump({"foo": "a\nb"}))
foo: 'a

  b'

|- を使っていないが改行がエスケープされているように見える。

Node.js(js-yaml)

Node.js v14.19.0を使用した。標準機能ではYAMLを扱うことができなそうだったので、"Node.js YAML"で検索して最初にできてたjs-yamlを利用した。

$ node
Welcome to Node.js v14.19.0.
Type ".help" for more information.
> const yaml = require('js-yaml')
undefined
> console.log(yaml.dump({"foo": "a\nb"}))
foo: |-
  a
  b

undefined
> console.log(yaml.dump({"foo": "a \nb"}))
foo: |-
  a
  b

undefined

エスケープされず差がなかった。

Rust

Rust playgroundを使い執筆時点のStableバージョンである1.71.0を使った。 Rustも標準機能ではYAMLが使えないのでserde_yamlを利用したが、どのバージョンが使われているかは確認していない。

コード

use std::collections::BTreeMap;

fn main() -> Result<(), serde_yaml::Error> {
  let mut m1 = BTreeMap::new();
  m1.insert("foo", "a\nb");
  println!("{}", serde_yaml::to_string(&m1)?);
  
  let mut m2 = BTreeMap::new();
  m2.insert("foo", "a \nb");
  println!("{}", serde_yaml::to_string(&m2)?); 
  
  Ok(())
}

出力

---
foo: "a\nb"

---
foo: "a \nb"

playgroundのリンクはこちら↓ https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cc48f996998c4a0cec9424c1a793d113

出力結果を見るとエスケープされていないように見える。 また、serde_yamlは|-を使わない実装になっていることがわかる。

Go

The Go Playgroundを使い執筆時点で最新の1.20を使った。

コード

package main

import (
    "fmt"

    "gopkg.in/yaml.v3"
)

func main() {
    m := map[string]string{}
    m["foo"] = "a\nb"
    d, _ := yaml.Marshal(m)
    fmt.Printf("%s", string(d))

    m = map[string]string{}
    m["foo"] = "a \nb"
    d, _ = yaml.Marshal(m)
    fmt.Printf("%s", string(d))
}

出力

foo: |-
    a
    b
foo: "a \nb"

playgroundのリンクはこちら↓ https://go.dev/play/p/Ylvd8kP4Bjg

出力結果はRubyと同じ挙動のようにみえる。

各言語での比較まとめ

  • エスケープされる: Ruby, Python, Go
  • エスケープされない: Node.JS(js-yaml), Rust(serde_yaml)

言語、ライブラリによって差が出るという興味深い結果になった。 この変数(オブジェクト)をYAMLでシリアライズするときの仕様はどこかにあるのだろうか? それとも参考実装があるのだろうか?

*1:yqコマンドで確認した

*2:YAML.dumpのline_widthオプションで制御できる

*3:PrometheusのAlertmanagerで意図しないアラートが飛ぶ問題が手元の環境であり、それを調べていたら今回のことを見つけた