知ってるようで知らない YAML のご紹介

はじめに

こんにちは、イノベーションセンター テクノロジー部門の小倉 (@Mahito) です。 普段は Engineer Empowerment Project のリーダーとして、エンジニアのはたらく環境を良くする取り組みや NTT Tech Conference や勉強会などのエンジニアが楽しく働くための取り組みをしています。

今回は社内で行った TechNight というイベントで発表した YAML の文法についての話を記事にしたものです。 もともとの発表は、YAML の記法について調べてるうちに「YAML こんなこと出来るのか」となったのでまとめたものでした。

YAML とは

公式サイト (The official YAML Web Site) ではこう書かれています。

YAML is a human friendly data serialization standard for all programming languages.

また、Wikipedia (YAML - Wikipedia) では初期の意味と現在の意味が違うと書いてありました。

YAMLは再帰的に定義された頭字語であり "YAML Ain't a Markup Language"(YAMLはマークアップ言語じゃない)の意味である。初期には "Yet Another Markup Language"(もうひとつ別のマークアップ言語)の意味と言われていたが、マークアップよりもデータ重視を目的としていたために後付されてできた名前である。

現在の最新バージョンは 1.2 で 12 年近く更新されていません。 (最後のパッチは 2009-10-01)

表記方法

階層構造の表現

YAML はインデントを使い階層構造で内容を表現します。ただし、インデントにはタブが使えずスペースのみが使えます。(インデントはスペース 2 個単位ですることが多いそうです)

始まりと終わり

YAML ドキュメントは 任意--- から開始して、... で終わらせることができます。 これは、YAML 形式の一部で、ドキュメントの最初と最後を示し、これらを用いることで 1 つのファイル内に複数の YAML ドキュメントを埋め込むことができます。

--- # やることリスト(ブロック形式)
- ブログを書く
- イベントの企画
- 技術調査・検証
...
--- # 買い物リスト(インライン形式)
[トマト, 卵, サラダ油]

1 つのドキュメント内に複数の YMAL ドキュメントが埋め込まれている場合、プログラミング言語によっては load/safe_load では 1 つの YAML ドキュメントしか読めない、またはそもそも読み込みできないものがあります。

  • (Ruby) では複数のうち先頭の 1 つの YAML ドキュメントしか読めません。
require 'yaml'

open('sample.yml', 'r') do |yml|
  p YAML.safe_load(yml)
end

# => ["ブログを書く", "イベントの企画", "技術調査・検証"]
  • Python では yaml.composer.ComposerError: expected a single document in the stream が返されます。
import yaml

with open('sample.yml') as f:
    docs = yaml.safe_load(f)
    for doc in docs:
        print(doc)

# => yaml.composer.ComposerError: expected a single document in the stream

1 つのドキュメント内に複数の YMAL ドキュメントがあるファイル読み込む際は load_stream(Ruby)や load_all (Python) を使うとまとめて読むことができます。

require 'yaml'

open('sample.yml', 'r') do |yml|
  p YAML.load_stream(yml)
end

# => [["ブログを書く", "イベントの企画", "技術調査・検証"], ["トマト", "卵", "サラダ油"]]

なお、--- を "directives end"、 ... を "document end" と呼ぶそうですが、わかりにくいということで、 --- を YAML 1.1 で使われいた "document start" という名前に戻そうという RFC が上がっています。

私は --- が YAML に書かれているのを見たことがありますし、何なら使ってもいましたが、「YAML の先頭にある記号でなくても良い」程度にしか意識してきていませんでした。さらに、... はまったく見たことがなかったので、今回調べて初めて知りました。

コメント

行頭、もしくはスペースの後に番号記号 "#" を指定すると、その後はコメントになります。

# 行頭コメント
[トマト, 卵, サラダ油] # これもコメント

リスト

- を並べることでリスト表現が可能です。

--- # 果物一覧
- りんご
- みかん
- いちじく
- もも

上記はブロック形式ですが、インデントを使わずにインライン形式で [..., ..., ] と表現することもできます。

--- # 果物一覧
[りんご, みかん,  いちじく, もも]

連想配列

key: value とすることで連想配列の表現が可能です。

--- # 果物と色の組み合わせ一覧
りんご:みかん: オレンジ
いちじく: 白と赤
もも: ピンク

# => {"りんご" => "赤", "みかん" => "オレンジ", "いちじく" => "白と赤",  "もも" => "ピンク"}

リスト同様にインライン形式で {key1: value1, key2: value2, } と表現することもできます。

{りんご: 赤, みかん: オレンジ, いちじく: 白と赤,  もも: ピンク}

key:value: の後ろにスペースを空けないと、文字として扱われてしまいます。

key:value

# => "key:value"

また、: のあとのスペースもしくは改行はマッピングに利用されるので次のような c: という表現はエラーとなります。

windows_drive: c:

# => (<unknown>): mapping values are not allowed in this context at line 1 column 17 (Psych::SyntaxError)

マッピング以外の意図で : を利用する際は、: の後ろにスペースや改行をいれない、もしくは ''"" で括れば利用できます。

windows_path: c:\windows
c_drive: 'c:'
d_drive: "d:"

# => {"windows_path"=>"c:\\windows", "c_drive"=>"c:", "d_drive"=>"d:"}

"" ではエスケープを使用できる点が、 '' との違いです。

single quote: 'エスケープが使えません\n'
double_quote: "エスケープが使えます\n"

# => {"single quote"=>"エスケープが使えません\\n", "double_quote"=>"エスケープが使えます\n"}

複雑なキーマッピング

YAML では "? " (? とスペース)を使うと複雑なキーを生成できます。

(出典)Example 2.11. Mapping between Sequences

? - Detroit Tigers
  - Chicago cubs
:
  - 2001-07-23

# => {["Detroit Tigers", "Chicago cubs"]=>[#<Date: 2001-07-23 ((2452114j,0s,0n),+0s,2299161j)>]}

上の ["Detroit Tigers", "Chicago cubs"] がキー、下の 2001-07-23 が Value となります。

同様に Key に連想配列も指定可能です。

? { New York Yankees: Atlanta Braves }
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]

# => {{"New York Yankees"=>"Atlanta Braves"}=>[#<Date: 2001-07-02 ((2452093j,0s,0n),+0s,2299161j)>, #<Date: 2001-08-12 ((2452134j,0s,0n),+0s,2299161j)>, #<Date: 2001-08-14 ((2452136j,0s,0n),+0s,2299161j)>]}

ちなみに "? " を外すと下の : 以下は無視されます。

{ New York Yankees: Atlanta Braves }
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]

# => {"New York Yankees"=>"Atlanta Braves"}

? を使うことで value を null にした連想配列の作成も可能です。

(出典)Example 2.25. Unordered Sets

---
? Mark McGwire
? Sammy Sosa
? Ken Griff

# => {"Mark McGwire"=>nil, "Sammy Sosa"=>nil, "Ken Griff"=>nil}

このあたりも YAML の公式ドキュメントを見て初めて知った機能です。 連想配列の key に配列や連想配列を利用したり、連想配列の value を null にするなど、今まで使ったことがない機能ですが知っておくといつの日にか使う機会が訪れるやもしれませんね。

Bool の扱い

Bool型値 (True/False) は複数形式で指定可能です。

a: yes
b: no
c: True
d: TRUE
e: false

# => {"a"=>true, "b"=>false, "c"=>true, "d"=>true, "e"=>false}

連想配列のキーに yestrue, no などを使うと想定しない動きをするので注意が必要です。

yes: "We can"

# => {true=>"We can"}

もし先頭を yes にする場合は文字として設定する必要があります。

"yes": "We can"

# => {"yes"=>"We can"}

複数行に分けた表現

文字列は、 | または > を使用して複数行に分けることができます。

| を使用して複数行に分けた場合には、改行と、末尾にあるスペースが含まれます。

pipe_lines: |
            複数行の文章は
            '|'  を使うことで
            楽に書けます。 

# => {"pipe_lines"=>"複数行の文章は\n'|'  を使うことで\n楽に書けます。 \n"}

> を使うと改行を折り返してスペースに置き換えます。

long_text: >
           長い文章は
           '>'を使うことで
           1行で書けます

# => {"long_text"=>"長い文章は '>'を使うことで 1行で書けます\n"}

どちらの場合もインデントは無視されます。

> を使う場合で改行をしたいときは以下の2つを用いて改行できます。

  • 改行だけの空白行
  • インデント後に値を記入
long_text_lines: >
    a
    b

    c
    d
      e
    f
# => {"long_text_lines"=>"a b\nc d\n  e\nf\n"}

|> の機能は知っていましたが、> の改行で「インデント後に値を記入」は知らない機能でした。 この機能はインデントを含めた改行になるのでちょっと使い方が限られますが知っておくと便利な仕様ですね。

タグと型

YAML は型を明示していない場合、暗黙的な型付けがされます。明示的な型付けは ! のシンボルを使ったタグで示されます。

date: 2021-09-02
not-date: !!str 2021-09-02

# => {
# "date"=>#<Date: 2021-09-02 ((2459460j,0s,0n),+0s,2299161j)>,
# "not-date"=>"2021-09-02"}

デフォルトのタグ URI は tag:yaml.org,2002: です。その中で定義されている型のタグは以下のとおりです。

Collection Types

  • map
  • omap
  • pairs
  • set
  • seq

Scalar Types

  • binary
  • bool
  • float
  • int
  • merge (省略記法 <<)
  • null
  • str
  • timestamp
  • value
  • yaml

参照Language-Independent Types for YAML™ Version 1.1

タグの URI を明示的に切り替える場合は %TAG ! tag:hoge.com,2002: のように設定します。

上記のようにデフォルトで定義されているタグの呼び出しは !!str のようにエクスクラメーションマーク 2 つ (!!) , アプリケーション特有のローカルタグの呼び出しはエクスクラメーションマーク 1 つ (!) と別れています。

例: omapを指定

---
!!omap
- yesterday: !!str 2021-09-01
- today:     !!str 2021-09-02
- tomorrow:  !!str 2021-09-03

# => {"yesterday"=>"2021-09-01", "today"=>"2021-09-02", "tomorrow"=>"2021-09-03"}

例: omapなし

---
- yesterday: !!str 2021-09-01
- today:     !!str 2021-09-02
- tomorrow:  !!str 2021-09-03

# => [{"yesterday"=>"2021-09-01"}, {"today"=>"2021-09-02"}, {"tomorrow"=>"2021-09-03"}]

例: mapを指定

--- 
!!map
- yesterday: !!str 2021-09-01
- today:     !!str 2021-09-02
- tomorrow:  !!str 2021-09-03

# => [{"yesterday"=>"2021-09-01"}, {"today"=>"2021-09-02"}, {"tomorrow"=>"2021-09-03"}]

余談

1.10 など末尾に 0 が入る数字を明示的に表現するためには文字にする必要があります。 !!float 型では末尾の 0 が表示されないので注意が必要です。

---
- 1.10
- !!float 1.10
- '1.10'

# => [1.1, 1.1, "1.10"]

タグと型の機能も全く知らない機能でした。 型の機能は調べている中で便利だと感じたのでこれから使っていきたい機能です。

アンカーとエイリアス

YAMLは & でアンカーを設定し、 * で参照が可能です。さらに << もしくは !!merge を使うことで、参照先の値の変更も可能です。

--- !!omap
- default: &default
    user: root
    password: password
    db_url: localhost:3306

- development: *default

- production:
    <<: *default
    db_url: production:3306

# => {"default"=>{"user"=>"root", "password"=>"password", "db_url"=>"localhost:3306"},
# "development"=>{"user"=>"root", "password"=>"password", "db_url"=>"localhost:3306"},
# "production"=>{"user"=>"root", "password"=>"password", "db_url"=>"production:3306"}}

上記の例では default というアンカーを設定し、development はそのまま参照、poroduction は db_url を変更をしています。

エイリアスとアンカーも見たことはあるけどうまく使えてなかった機能です。 今回調べたことで理解が深まったので今後は使っていきたいです。

YAMLの今後

ここまで紹介してきた現在のバージョンは "YAML 1.2 - 3rd Edition, Patched at 2009-10-01" です。 このバージョンは 12 年近く更新されていませんでしたが、今年の 5 月に突然次期バージョンへのRFCが始まっています。

GitHub 上で Template をベースに PR 形式で RFC を受け付けているので、興味があればリポジトリを覗いてみて、提案があれば RFC を書いてPRをしてみてはいかがでしょう。

まとめ

普段何気なく使っている YAML ですが、まだ知らない、うまく使えていない機能がありました。 この記事では YAML のすべてを紹介できてはいませんが、もしこの記事を読んで YAML に興味を持っていただけたのであれば、一度 YAML の公式ページ を読んでみることをおすすめします。 また、YAML の新しいバージョンが動き出しているので、そちらも興味があればチェックしてみてはいかがでしょう。

参考

© NTT Communications Corporation All Rights Reserved.