C# で Java の properties ファイルを読む

# **はじめに**

Stack Overflow に _“[Can .NET load and parse a properties file equivalent to Java Properties class?](https://stackoverflow.com/q/485659/1174054)” (January, 2009)_ という質問があり、Java の properties ファイルを C# で読む方法を尋ねている。質問者のように、既存の properties ファイルに変更を加えずにそのまま C# で読みたいというケースは存在する。

当該質問にはコード付きの回答が幾つか寄せられているが、[私の回答](https://stackoverflow.com/a/48072119/1174054)を除き、いずれの実装も実用には耐えられない。存在しない仕様を勝手に追加しているものまである。`;` や `'` をコメント行開始文字として扱ったり、プロパティー値を囲む引用符を勝手に削除したり。

やはり自作するしかない。


# **Properties ファイルの仕様**

Java properties ファイルの仕様は <code>[java.util.Properties.load(java.io.Reader)](https://docs.oracle.com/javase/9/docs/api/java/util/Properties.html#load-java.io.Reader-)</code> の JavaDoc に書かれている。問題は、みんなが想像するよりも少し仕様が複雑だということだ。全てを網羅しているわけではないが、特に考慮すべき点は次のとおり。

1. 行には、_自然行_と_論理行_の二種類がある。
2. 自然行は `\n`, `\r`, `\r\n` もしくはストリームの末尾で区切られる。
3. 論理行は、バックスラッシュを使用して行末記号シーケンスをエスケープすることで、隣接する複数の自然行にまたがることがある。
4. 論理行内の二番目以降の自然行の先頭の空白文字群は捨てられる。
5. 空白文字とは、スペース (` `, `\u0020`)、タブ (`\t`, `\u0009`)、ラインフィード (`\f`, `\u000C`) である。
6. 仕様で明確に述べられているように、_「行末記号がエスケープされているかどうかを判定する場合、行末記号シーケンスの前の文字を調べるだけでは十分ではありません。行末記号がエスケープされるためには、連続した奇数のバックスラッシュが存在する必要があります。入力は左から右に処理されるため、行末記号の前(またはほかの場所)に連続したバックスラッシュが2n (ゼロでない偶数)個存在する場合、エスケープ処理後にn個のバックスラッシュがエンコードされます。」_
7. `=` がキーと値の区切り文字として使用される。
8. `:` もキーと値の区切り文字として使用される。
9. キーと値の間の区切り文字は省略可能。
10. コメント行は、最初の非空白文字として `#` もしくは `!` を持つ。つまり、`#` や `!` の前の空白文字群は許容される。
11. 行末記号をバックスラッシュでエスケープしても、コメント行を複数行にまたがらせることはできない。
12. 仕様で明確に述べらているとおり、`=` や `:`、空白文字もバックスラッシュでエスケープすることによりキーに埋め込むことができる。
13. 行末記号でさえ、`\r` と `\n` というエスケースシーケンスを用いることで含めることができる。
14. 値が省略された場合、空文字列として扱う。
15. `\uxxxx` は Unicode 文字をあらわす。
16. 有効なエスケープ文字を構成しない文字の前に置かれたバックスラッシュはエラーとして扱われず、単にドロップされる。


# **Properties ファイルの例**

そういうわけで、例えば `test.properties` が次の内容を持っている場合

```
# A comment line that starts with '#'.
   # This is a comment line having leading white spaces.
! A comment line that starts with '!'.

key1=value1
  key2 : value2
    key3 value3
key\
  4=value\
    4
\u006B\u0065\u00795=\u0076\u0061\u006c\u0075\u00655
\k\e\y\6=\v\a\lu\e\6

\:\ \= = \\colon\\space\\equal
```

次のキーバリューペア群として解釈されるべきである。

| キー | 値 |
|:--|:--|
| `key1` | `value1` |
| `key2` | `value2` |
| `key3` | `value3` |
| `key4` | `value4` |
| `key5` | `value5` |
| `key6` | `value6` |
| `: =`  | `\colon\space\equal` |


# **コード例**

[NuGet](https://www.nuget.org/) パッケージ「[Authlete.Authlete](https://www.nuget.org/packages/Authlete.Authlete)」に含まれる `PropertiesLoader` クラスは Properties ファイルの仕様を解釈することができる。次のコード例は

```csharp
using System;
using System.IO;
using System.Collections.Generic;
using Authlete.Util;

namespace MyApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            string file = "test.properties";
            IDictionary<string, string> properties;

            using (TextReader reader = new StreamReader(file))
            {
                properties = PropertiesLoader.Load(reader);
            }
        
            foreach (var entry in properties)
            {
                Console.WriteLine($"{entry.Key} = {entry.Value}");
            }
        }
    }
}
```

次の出力を生成する。

```
key1 = value1
key2 = value2
key3 = value3
key4 = value4
key5 = value5
key6 = value6
: = = \colon\space\equal
```

Java での同様のコードは次のとおり。

```java
import java.util.*;
import java.io.*;

public class Program
{
    public static void main(String[] args) throws IOException
    {
        String file = "test.properties";
        Properties properties = new Properties();

        try (Reader reader = new FileReader(file))
        {
             properties.load(reader);
        }

        for (Map.Entry<Object, Object> entry : properties.entrySet())
        {
            System.out.format("%s = %s\n", entry.getKey(), entry.getValue());
        }    
    }
}
```


# **ソースコード**

<code>[PropertiesLoader.cs](https://github.com/authlete/authlete-csharp/blob/master/Authlete/Util/PropertiesLoader.cs)</code> のソースコードは [authlete-csharp](https://github.com/authlete/authlete-csharp) 内に存在する。また、`PropertiesLoader` 用の [xUnit](https://xunit.github.io/) テスト群は <code>[PropertiesLoaderTest.cs](https://github.com/authlete/authlete-csharp/blob/master/Authlete.Tests/Util/PropertiesLoaderTest.cs)</code> 内に記述されている。