YATT::Lite::XHF::Syntax(ja) - Extended Header Fields (XHF) フォーマットの構文定義
require YATT::Lite::XHF; my $parser = YATT::Lite::XHF->new(string => <<'END'); # Taken from http://docs.ansible.com/YAMLSyntax.html#yaml-basics name: Example Developer job: Developer skill: Elite employed: 1 foods[ - Apple - Orange - Strawberry - Mango ] languages{ ruby: Elite python: Elite dotnet: Lame } name: hkoba languages{ yatt: Elite? } END # read() は(\n\n+ で区切られた)一パラグラフ分のレコードを返します, # リストコンテキストでは, 戻り値はフラットなリストです。 # 通常、それは キー: 値 組のリストですが、それ以外のものを含めることも出来ます。 # スカラコンテキストで呼び出した場合は hash が返ります while (my %hash = $parser->read) { print Dumper(\%hash), "\n"; }
Extended Header Fields (XHF) フォーマットとは hkoba が独自に定義した俺々フォーマットで、 電子メールのヘッダ や HTTP の ヘッダ をベースに、 入れ子データ構造 のための拡張を加えたものです。 XHF 形式のファイルや文字列を load/parse するには YATT::Lite::XHF を使って下さい。
Note: 元々 XHF は unit test のテスト入力・出力ペアを簡単に書くために 設計されました。シリアライザ (YATT::Lite::XHF::Dumper) も用意してありますが、 決して完全な(perl の任意のデータ構造をシリアライズすることを目指した)ものでは ありません。そのような用途には YAML 一族や, Storable などを用いて下さい。
最も単純な用途では、 YAML と XHF はとても良く似て見えます。例えば、 {foo => 1, bar => 2}
というデータを表現する時、YAML と XHF は同じように書けます。
foo: 1 bar: 2
しかし、少し複雑なデータ構造になると、段々違いが見えてきます。 例えば {x => [1, 2, "3, 4"], y => 5}
、
XHF では以下のように書けます:
{ x[ - 1 - 2 - 3, 4 ] y: 5 }
同じものを YAML ではこう書くでしょう:
--- x: - 1 - 2 - '3, 4' y: 5
両者の違いは:
{} []
を用い、YAML は 字下げ を使って構造を表す。3, 4
はそのまま表記できるが、YAML では エスケープして '3, 4'
と書かねばならない。
XHF の キー:値
ペアの 値 部分では、エスケープする必要がある文字は 改行文字 \n
(と、値の前後の空白文字) だけです。 つまり、値部分には全く「構文」がありません。ですので、 値部分を書く時に覚えなければならないことは、実質的に無いも同然なのです。
単純に、全ての改行 "\n"
を改行+SPACE "\n "
で置換して下さい。 つまり s/\n/\n /g
するだけです。
例: { foo => "1\n2\n\n3", bar => 4 }
は以下のように書けます:
foo: 1 2 3 bar: 4
キーと値の区切りを、 ": "
の代わりに ":\n"
で書くだけです。 後は他と同様、値の中の "\n"
をエスケープするだけで構いません。
例: { foo => " x ", bar => "\n\ny\n\n" }
は以下のように書けます:
foo: x bar: y
XHF のキー部分には、書ける文字に強い制限が有ります(値部分と違って). 具体的には [[:alnum:]]
, "-"
, "."
, "/"
そして 幾つかの追加の文字(詳細は "BNF" の field-name の定義を参照) です。
しかしながら、 キー: 値
と書く代わりに、- キー
, - 値
と2つのアイテムを 連続して書くことで、同じ意味を表すことが出来ます。この2つの書き方は、 どこでも入れ替えて使うことが出来ます。 ですので、もし面倒なら、キー部分を全部 - キー
表記で書いて、 (値と同じく)改行 \n
だけエスケープする、でも構いません。
# 例えば、以下のブロック: foo: 1 bar: 2 # 上記は、以下と同じ結果になります - foo - 1 - bar - 2
例: { "foo bar" => "baz" }
は以下のように書けます:
{ - foo bar - baz }
別の例: { "\n foo\nbar \n" => "baz" }
は以下のように書けます。
{ - foo bar - baz }
入れ子になったデータ構造でも、同じ法則が適用できます。
foo{ x: 1 y: 2 } baz[ - z ] # can be written instead as following: - foo { x: 1 y: 2 } - baz [ - z ] # or even like following: - foo { - x - 1 - y - 2 } - baz [ - z ]
キー: 値
記法は配列の中でも使えます:
[ foo: 1 bar: 2 ] # above is equal to following [ - foo - 1 - bar - 2 ]
もう一つの重要な違いは、コンテナの型の選択方法です。 XHF では キー・値を区切る区切り文字は、"値部分の型" を決めるものです。 これは区切り文字が外側のコンテナの型を決める YAML とは大きく違います。
XHF では、以下のブロック:
foo: 1 bar: 2
は、( foo => 1, bar => 2 )
つまり4つの要素からなるフラットなリスト、を表します。このリストをどんなコンテナに格納するのかは、呼び出し側が自由に決めて構いません。 例えば辞書に格納したいなら:
my %dict = $parser->read;
配列に格納したいなら:
my @array = $parser->read;
なお、もしスカラコンテキストで呼び出した場合は、辞書が作られます (もし奇数個しか要素がないなら、エラーになります).
my $dict = $parser->read;
これに対して YAML では :
は常に map(dictionary) を意味します。 ですので、上記は +{ foo => 1, bar => 2 }
を返すでしょう。
以上のように XHF のブロックの一番外側はフラットなリストを表しているので、 そこには (Perl の HASH データ構造から来る) 制限は適用されません。 従って、Perl の HASH では許されないような、キーの順序や、キーの重複を 表現することも、以下のように一応可能です:
foo: 1 foo: 2 foo: 3 bar: x bar: y
上記を以下のスクリプトで読むと
my @array = $parser->read;
@array
には (foo => 1, foo => 2, foo => 3, bar => 'x', bar => 'y')
が格納されるでしょう。
この機能は、ある種のテストデータを表現するために、時に大いに役立ちます。 (例えば HTTP のクエリ引数や、 Email ヘッダの "Received" フィールド). 例えば上記の XHF が表すデータと 等価な http クエリパラメータを作る html form は、以下のように書けるでしょう:
<input type="checkbox" name="foo" value="1"> <input type="checkbox" name="foo" value="2"> <input type="checkbox" name="foo" value="3"> <input type="checkbox" name="bar" value="x"> <input type="checkbox" name="bar" value="y">
Note: 現在のところ、入れ子要素は perl の普通の HASH と ARRAY に変換されます。 ですので、上記の順序・重複保護が有効に働くのは、一番外側のみです。
XHF の入力ストリームは連続空行 "\n\n+"
で区切られます (これは Email ヘッダや HTTP ヘッダと同様です)。 これは Perl の(懐かしの) パラグラフモードに基づく複数行レコードのフォーマットが ベースになっています。Perl でパラグラフモードを扱うためのマニュアルは perl -00 や Setting $RS to "" を参照して下さい。
Note: XHF では "コメントのみを含む" ブロックは自動的に読み捨てられます。例えば:
foo: 1 bar: 2 # どや! ここにコメントだけのブロックがおんねん! baz: 3 qux: 4
上記を以下のスクリプトで読むとします:
my @records; push @records, $_ while $_ = $parser->read;
すると @records
には2つの要素 ({foo => 1, bar => 2}, {baz => 3, qux => 4})
だけが入ります。
ごく稀に、一つのストリームに、メタ情報的なものを含めたり、含めなかったり! したくなる時が有ります。そんな時は、ストリームの先頭で、一回だけ read(skip_comment => 0)
のように、 "コメントのみブロック"の読み捨て機能をオフにして読み込む、というハックが使えます:
# これがメタ情報. 下の test => 1 を有効にするには、次の行頭の "# " を削って下さい # test: 1 # This is body1 foo: 1 bar: 2 # This is body2 foo: 3 bar: 4
スクリプトはこんな感じです。
if (my @meta = $parser->read(skip_comment => 0)) { # process metainfo. You may get (test => 1). } while (my @content = $parser->read) { # process body1, body2, ... }
Here is a more dense example in XHF:
name: hkoba # (1) You can write a comment line here, starting with '#'. job: Programming Language Designer (self-described;-) skill: Random employed: 0 foods[ - Sushi #(2) here too. You don't need space after '#'. This will be good for '#!' - Tonkatsu - Curry and Rice [ - More nested elements ] ] favorites[ # (3) here also. { title: Chaika - The Coffin Princess # (4) ditto. heroine: Chaika Trabant } { title: Witch Craft Works heroine: Ayaka Kagari # (5) You can use leading "-" for hash key/value too (so that include any chars) - Witch, Witch! - Tower and Workshop! } # (6) You can put NULL(undef) like below. (equal space sharp+keyword) = #null ]
Above will be loaded like following structure:
$VAR1 = { 'foods' => [ 'Sushi', 'Tonkatsu', 'Curry and Rice', [ 'More nested element' ] ], 'job' => 'Programming Language Designer (self-described;-)', 'name' => 'hkoba', 'employed' => '0', 'skill' => 'Random', 'favorites' => [ { 'heroine' => 'Chaika Trabant', 'title' => 'Chaika - The Coffin Princess' }, { 'title' => 'Witch Craft Works', 'heroine' => 'Ayaka Kagari', 'Witch, Witch!' => 'Tower and Workshop!' }, undef ] };
Above will be written in YAML like below (note: inline comments are omitted):
--- employed: 0 favorites: - heroine: Chaika Trabant title: 'Chaika - The Coffin Princess' - 'Witch, Witch!': Tower and Workshop! heroine: Ayaka Kagari title: Witch Craft Works - ~ foods: - Sushi - Tonkatsu - Curry and Rice - - More nested element job: Programming Language Designer (self-described;-) name: hkoba skill: Random
This YAML example clearly shows how you need to escape strings quite randomly, e.g. see above value of $VAR1->{favorites}[0]{title}
. Also the key of $VAR1->{favorites}[1]{'Witch, Witch!'}
is nightmare.
I don't want to be bothered by this kind of escaping. That's why I made XHF.
XHF は(空行で区切られた)パラグラフ一個ずつの単位でパースされます。 個々のパラグラフは複数の xhf-item
を含むことが出来ます。 全ての xhf-item は必ず行の先頭から始まり、最後は改行で終わります。 xhf-item は以下の2つのうち、どちらかの形式を取ります。
<name> <type-sigil> <sep> <value> (name-value pair) <type-sigil> <sep> <value> (standalone value)
type-sigil
は value
の型を決めます。 sep
は空白文字(スペース、タブ、改行) のいずれかです。 sep
が改行の場合は 書いたままモード(verbatim text)) を意味します。 ただし dict/array ブロックの場合は、sep
は改行だけしか許されません。
type-sigil
の全リストと、使える sep
は以下の通りです。
"name:"
then " "
or "\n"
":"
は文字列値を(名前付きで)格納します。名前の省略は出来ません。 sep
は全ての空白文字です。
"-"
then " "
or "\n"
"-"
は文字列値を(名前抜きで)格納します。名前は書いてはいけません。
(Note: Currently, ","
works same as "-"
. This feature is arguable.)
"{"
then "\n"
"name{"
then "\n"
"{"
は辞書ブロック ( { %HASH }
コンテナ)の始まりです。 名前を付けることが出来ます。
このブロックは "}\n"
で閉じられねばなりません。 また、要素の個数は偶数個でなければなりません。
"["
then "\n"
"name["
then "\n"
"["
は配列ブロック( [ @ARRAY ]
コンテナ)の始まりです。 名前をつけることが出来ます。
このブロックは "]\n"
で閉じられねばなりません。
"="
then " "
or "\n"
"name="
then " "
or "\n"
"="
は特別な値を格納するために使います。 名前を付けることが出来ます。
現状では、#undef
と、同じ意味の別表記である #null
だけが定義されています。
"#"
"#"
は埋め込みコメントを格納するために使います。 名前を付けることは出来ません。
Here is a syntax definition of XHF in extended BNF (roughly following ABNF.)
xhf-block = 1*xhf-item xhf-item = field-pair / single-text / dict-block / array-block / special-expr / comment field-pair = field-name field-value field-name = 1*NAME *field-subscript field-subscript = "[" *NAME "]" field-value = ":" text-payload / dict-block / array-block / special-expr text-payload = ( trimmed-text / verbatim-text ) NL trimmed-text = SPTAB *( 1*NON-NL / NL SPTAB ) verbatim-text = NL *( 1*NON-NL / NL SPTAB ) single-text = "-" text-payload dict-block = "{" NL *xhf-item "}" NL array-block = "[" NL *xhf-item "]" NL special-expr = "=" SPTAB known-specials NL known-specials = "#" ("null" / "undef") comment = "#" *NON-NL NL NL = [\n] NON-NL = [^\n] SPTAB = [\ \t] NAME = [0-9A-Za-z_.-/~!]
field-name can contain /
, .
, ~
and !
. Former two are for file names (path separator and extension separator). Later two (and field-subscript) are incorporated just to help writing test input/output data for YATT::Lite, so these can be arguable for general use.
If field-name is separated by ": "
, its field-value will be trimmed their leading/trailing spaces/tabs. This is useful to handle hand-written configuration files.
But for some software-testing purpose(e.g. templating engine!), this space-trimming makes it impossible to write exact input/output data.
So, when field-sep is NL, field-value is not trimmed.
Currently, I'm not so rigid to reject the use of CRLF. This ambiguity may harm use of XHF as a serialization format, however.
","
can be used in-place of "-"
.This feature also may be arguable for general use.
":"
without name
was valid, but is now deprecated.Previously valid
: bar
which represents ( "" => "bar" )
, is now invalid. Please use two "- "
items like following:
- - bar
XXX: Hmm, should I provide deprecation cycle? Are there someone already used XHF to serialize important data even before having this manual? If so, please contact me. I will add an option to allow this.
Although line-continuation is obsoleted in HTTP headers, line-continuation will be kept valid in XHF spec. This is my preference.
"KOBAYASI, Hiroaki" <hkoba@cpan.org>
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.