2008-07-20

Boost.Spiritでオプションファイル解析

Boost.Spiritでオプションファイル解析。

アプリケーションに渡すちょっとしたオプション指定ファイルを作ろうと思ったので、boost::spiritで構文解析器を書いてみることにしました。

まずは文法を決めなければなりません。

OptionA 123
OptionB "ABCDE"

みたいな単純なkeyとvalueの組み合わせでも良いんですが、もう少し複雑な構造を表現しようとすると、少し面倒なことになってしまいます。

OptionC_Item0 "AAAA"
OptionC_Item1 "BBBB"
OptionC_Item2 "CCCC"

OptionD_Item0_Name "AAAA"
OptionD_Item0_Type 0
OptionD_Item1_Name "BBBB"
OptionD_Item1_Type 0
OptionD_Item2_Name "CCCC"
OptionD_Item2_Type 1
OptionD_Item2_Value 123

なので、値の部分にタプルとマップを指定できるようにしようと思います。
タプルは値の有限の列、マップは識別子から値への対応関係とします。
タプルとマップでOptionCとOptionDを書き直すと次のようになります。

OptionC_Items ["AAAA" "BBBB" "CCCC"]

OptionD_Items [
  {Name "AAAA" Type 0}
  {Name "BBBB" Type 0}
  {Name "CCCC" Type 1 Value 123}
]

よく見ると、このファイル全体も、{ }で囲まれては居ませんが一種のマップのようなものです。このあたりはまとめられそうです。

文法をまとめます。

option-file ::= map-body オプションファイルはマップの{ }内の中身と同じです。
map ::= '{' map-body '}' マップは{ }とその中身です。
map-body ::= map-element* マップの中身は0個以上のマップ要素です。
map-element ::= map-key map-value マップ要素はマップキーとマップ値の対です。
map-key ::= ident マップキーは識別子(Identifier)です。
map-value ::= value マップ値は任意の値です。
tuple ::= '[' tuple-body ']' タプルは[ ]とその中身です。
tuple-body ::= tuple-element* タプルの中身は0個以上のタプル要素です。
tuple-element ::= value タプル要素は任意の値です。
value ::= integer | string | map | tuple 値は整数、文字列、マップ、タプルのいずれかです。
integer ::= [1-9][0-9]* 整数は1から9で始まる数字です。
string ::= '"' [^"n]* '"' 文字列は"で囲まれた文字の列です。
ident ::= [a-zA-Z_][a-zA-Z0-9_]* 識別子はアルファベットやアンダーバーで始まる文字列です。
  • ※字句間は自由に空白、タブ、改行が許される。
  • ※字句間の';'は行末まではコメント。

以上で文法がきまりましたので、これをboost::spiritで解析しようと思います。

struct OptionFileGrammer : public boost::spirit::grammar<OptionFileGrammer>
{
  template<typename ScannerT>
  struct definition
  {
    typedef boost::spirit::rule<ScannerT> rule_t;

    rule_t opt_file;
    rule_t map, map_body, map_element, map_value, map_key;
    rule_t tuple, tuple_body, tuple_element;
    rule_t value;
    rule_t integer_l;
    rule_t string_l;
    rule_t ident;

    definition(const OptionFileGrammer &self)
    {
      using namespace boost::spirit;

      opt_file = map_body;

      map = ch_p('{') >> map_body >> ch_p('}');
      map_body = *map_element;
      map_element = map_key >> map_value;
      map_value = value;
      map_key = ident;

      tuple = ch_p('[') >> tuple_body >> ch_p(']');
      tuple_body = *tuple_element;
      tuple_element = value;

      value = integer_l | string_l | map | tuple;

      integer_l  = int_p;
      string_l   = lexeme_d[ confix_p('"', *c_escape_ch_p, '"') ];

      ident = lexeme_d[ (alpha_p | ch_p('_')) >> *(alnum_p | ch_p('_'));

    }

    const rule_t &start() const { return opt_file;}
  };
};


int main(int argc, char **argv)
{
  std::string s;

  // sへ読み込み。


  OptionFileGrammer optGr;
  parse_info<std::string::const_iterator> info = parse(s.begin(), s.end(), optGr >> end_p, space_p | comment_p(';'));
  if(!info.full){
    std::cout << "Parse Error." << std::endl;
    return -1;
  }
}
        

definitionの中はわりと文法定義そのものです。
整数値はint_pそのままを、文字列はconfix_pc_escape_ch_pというおあつらえ向きなものがありました。元の文法では抜けていましたが、符号やエスケープシーケンスにも対応しましょう。

スキップパーサーのおかげで、空白スキップ、コメントスキップも簡潔に書けます。
ここではparseの引数にspace_p | comment_p(';')を指定することで実現しています。

しかし、スキップパーサーのおかげで、string_lやidentの定義でハマりました。例えばA128は有効な識別子ですが、スキップパーサーのおかげでA12(空白)8もA128と同等に扱われてしまいます。これを回避するには、lexeme_d[ ]で囲って、スキップパーサーを一時的に無効にすればいいようです。

終端前の空白文字が残ってしまいフルマッチしない現象にも遭遇しました。これはここのNovember 28, 2007に書いてありますが、Boost1.34以降で変わった挙動らしく、end_pを最後に置けばいいようです。

以上でとりあえず構文解析ができるようになりました。

ただ、これだけでは入力テキストが文法にマッチするか否かが分かるだけなので、本来の目的は達成できていません。
このオプションの指定値は何?といったアプリケーション側からの問い合わせに答えられるよう、何らかのデータ構造を構築する必要があります。

そのあたりはまだよく調べていないので次の機会に。セマンティックアクションでゴリゴリやればいいのか、それとも直接的に木を構築する方法が用意されているのか。