便乗して PowerShell で汎用的に使えそうな設定ファイルとパーサ作ってみた

下記の記事が元ネタです。便乗して自分も作ってみました。実装は[code](#code)の節参照。

* [[Powershell]汎用的に使えそうな設定ファイルとパーサ作ってみた - Qiita](http://qiita.com/S_SenSq/items/e96c8b8ccf6da6553ed0)

なお、今回作った記法を以下ではPOMPと表記します。（TOMLの記法をパクりまくったので、POMP(Powershell de tOMl wo Pakutta の略)です。ネタです。）

* [TOML仕様和訳 - Qiita](http://qiita.com/minoritea/items/c0de47b8beb813c655d4)


# 例

だいたいTOMLと同じように書けるので、次節以降はTOMLと差異の大きい部分だけ説明します。詳細は、[code](#code)の節にある正常系テストを確認してください。


```toml:example.pomp.txt
# POMPドキュメントの例です。

title = POMP_Example

[owner]
	name = "Lance Uppercut"
	dob : datetime = 1979-05-27T07:32:00-08:00

[database]
	server = 192.168.1.1
	connection_max : int = 5000
	enabled : bool = true

	[[database.ports]] : int
		8001
		8001
		8002

[clients]
	[[clients.data]]
		[[clients.data.]]
			'gamma'
			"delta"
		[[clients.data.]]:int
			1
			2

# タブもしくはスペースで自由にインデントできますが、
# 必ずしも必要なものではありません。
[servers]
	[servers.alpha]
	    ip = 10.0.0.1
	    dc = eqdc10


[servers.beta]
ip = 10.0.0.2
dc = eqdc10
```

上記POMPドキュメントは次のコマンドを用いて、

```.ps1
Get-Content .\example.pomp.txt -Raw | .\ConvertFrom-Pomp.ps1 | ConvertTo-Json -Depth 10
```

下記のJSONに変換できます。


```.json
{
    "title":  "POMP_Example",
    "owner":  {
                  "name":  "Lance Uppercut",
                  "dob":  "\/Date(296667120000)\/"
              },
    "database":  {
                     "server":  "192.168.1.1",
                     "connection_max":  5000,
                     "enabled":  true,
                     "ports":  [
                                   8001,
                                   8001,
                                   8002
                               ]
                 },
    "clients":  {
                    "data":  [
                                 [
                                     "gamma",
                                     "delta"
                                 ],
                                 [
                                     1,
                                     2
                                 ]
                             ]
                },
    "servers":  {
                    "alpha":  {
                                  "ip":  "10.0.0.1",
                                  "dc":  "eqdc10"
                              },
                    "beta":  {
                                 "ip":  "10.0.0.2",
                                 "dc":  "eqdc10"
                             }
                }
}
```

# 文字列

TOMLにある複数行文字列／複数行リテラル文字列は、POMPにはありません。
基本文字列、リテラル文字列はほとんどそのまま使えます。

なお、リテラル文字列内でシングルクォート(')を２個書くことで下記のようにエスケープされます。（そのため、複数行リテラル文字列のように書くとシングルクォートで囲まれた文字列として取得されます。）

```.ps1
PS > "str = '''escape'''" | .\ConvertFrom-Pomp.ps1

str               
---               
'escape'
```

## 裸リテラル文字列

POMPでは、クォート（"）、シングルクォート（'）で囲まれていないものについても、文字列として扱われる可能性があります。

クォートを使わない場合、何を持って文字列として扱うかは悩ましいところですが、実装としては、次の正規表現にマッチしたものを裸リテラル文字列として扱っています。

* `\w([^\s#]*\w)?`
  * [正規表現での文字クラス - 単語に使用される文字: \w](https://msdn.microsoft.com/ja-jp/library/20bw873z(v=vs.110).aspx#WordCharacter)
  * [正規表現での文字クラス - 空白文字: \s](https://msdn.microsoft.com/ja-jp/library/20bw873z(v=vs.110).aspx#WhitespaceCharacter)


# テーブル

TOMLは上位のテーブルの定義を省略できますが、POMPは上位のテーブル（または配列）の定義を省略できません。また、POMPはインラインテーブルを書くことはできません。

# 配列

POMPでは二重角括弧が配列の宣言になります。これは、一重角括弧がテーブルの宣言になることに対応しています。

TOMLとは二重角括弧の使い方が異なっているので注意してください。また、POMPにおいて、配列は名前付きの子要素が持てず、テーブルは名前付きの子要素しか持てません。

具体的には、下記のような記法になります。

```toml:.pomp
# 配列
[[arr1]]
One
Two
Three

# ジャグ配列
[[arr2]]
	[[arr2.]]
		1
		2
	[[arr2.]]
		3
		4
		5

# テーブルの配列
[[arr3]]
	[arr3.]
		foo = val1
		bar = val2
	[arr3.]

	[arr3.]
		baz = val3
```

上記のPOMPドキュメントは下記のJSONと等価です。

```.json
{
    "arr1": ["One", "Two", "Three"],
    "arr2": [["1", "2"], ["3", "4", "5"]],
    "arr3": [{"foo": "val1", "bar": "val2"}, {}, {"baz": "val3"}]
}
```

# 値の変換

`as`演算子を用いた、文字列から任意のデータ型への変換を実装しています。
なお、「任意のデータ型への変換を介して、パース時にヤバイ処理が実行されると、危険だよなー。」と思いながら実装しました。

値の変換は下記の通り、配列の宣言時か、Key／Valueペアを書く際に宣言し、コロン（:）の後に変換先の型を記載します。

```toml:.pomp
[[arr]]:bool
  true
  false

[table]
  x:int = 100
  y:float = 3.14
  z:datetime = 1979-05-27T07:32:00-08:00
```

## null値への変換

変換の結果としてnull値になる場合、通常はパースエラーとなります。
ただ、下記の通り、変換先の型として`null`を指定するか、もしくは変換先の型の末尾に疑問符（?）を指定した状態で、値として裸リテラル文字列で`null`と記載した場合、null値が許容されます。

```toml
[table]
  x:null = null
  y:int? = null
  z:string? = 'null'
```

上記のPOMPドキュメントは下記のJSONと等価です。

```.json
{"table" : {"x" : null, "y" : null, "z" : "null"}}
```


# code

下記の通り、Gistに投稿した。

* [Convert-FromPomp](https://gist.github.com/yumura/6fc37462b004df40b6d898d9368ba579#file-convertfrom-pomp-ps1)
* [正常系テスト](https://gist.github.com/yumura/6fc37462b004df40b6d898d9368ba579#file-convertfrom-pomp-tests-ps1)
* [異常系テスト](https://gist.github.com/yumura/6fc37462b004df40b6d898d9368ba579#file-convertfrom-pomp-error-tests-ps1)
* [変換記法テスト](https://gist.github.com/yumura/6fc37462b004df40b6d898d9368ba579#file-convertfrom-pomp-converter-tests-ps1)