C ++でiniファイル用のパーサーを作成する

この記事では、パーサーのiniファイルをC ++で記述する方法を説明します。 前回の記事で構築た文脈自由文法を基礎として取り上げます。 パーサーを構築するには、 Boost Spiritライブラリを使用します。これにより、 パーサーコンビネーターを使用して既製のプリミティブパーサーを組み合わせて独自のパーサーを構築できます。

重要:この記事は、読者がC ++の基本(STLの使用を含む)に精通していることを前提としています。 自分に自信がない場合は、C ++およびSTLの初心者向けの記事を最初に読むことをお勧めします。


文法


最初に、前の記事で作成したiniファイルの文法を思い出しましょう。
inidata = spaces, {section} .
section = "[", ident, "]", stringSpaces, "\n", {entry} .
entry = ident, stringSpaces, "=", stringSpaces, value, "\n", spaces .
ident = identChar, {identChar} .
identChar = letter | digit | "_" | "." | "," | ":" | "(" | ")" | "{" | "}" | "-" | "#" | "@" | "&" | "*" | "|" .
value = {not "\n"} .
stringSpaces = {" " | "\t"} .
spaces = {" " | "\t" | "\n" | "\r"} .

彼女の説明がすぐに必要です。

C ++およびBoost Spirit



ブーストをインストールすることから始めます(公式Webサイトで入手するか、OSの既製のパッケージを探してください)。 すべてのスピリットはヘッダーにあるため、ブーストを収集する必要はありません。 異なるシステムのインストールプロセスは異なる場合があるため、ここでは説明しません。

C ++でパーサーを作成するプロセスを詳細に説明しようとします。 ただし、この記事の目的ではないため、パフォーマンスについては特に考えません。

必要なヘッダーを接続することから始めましょう。
1 #include <fstream>
2 #include <functional>
3 #include <numeric>
4 #include <list>
5 #include <vector>
6 #include <string>
7
8 #include <boost/spirit.hpp>
9 #include <boost/algorithm/string.hpp>
10
11 using namespace std;
12 using namespace boost::spirit;

Spirit自体のヘッダーに加えて、boostのラインアルゴリズムのライブラリを含めました(trim関数を使用します)。 名前空間の構成の使用は常に良い習慣とは限りませんが、ここでは簡潔にするために許可します。

データタイプを定義します。レコードはキーと値のペア、セクションはレコードのキーリスト、すべてのiniファイルデータはセクションのリストです。
14 typedef pair<string, string> Entry;
15 typedef list<Entry > Entries;
16 typedef pair<string, Entries> Section;
17 typedef list<Section> IniData;

データ型に加えて、パーサーが次の非ターミナルを解析するときに呼び出されるイベントハンドラーが必要です。
19 struct add_section
20 {
21 add_section( IniData & data ) : data_(data) {}
22
23 void operator ()( char const * p, char const * q) const
24 {
25 string s(p,q);
26 boost::algorithm::trim(s);
27 data_.push_back( Section( s, Entries() ) );
28 }
29
30 IniData & data_;
31 };
32
33 struct add_key
34 {
35 add_key( IniData & data ) : data_(data) {}
36
37 void operator ()( char const * p, char const * q) const
38 {
39 string s(p,q);
40 boost::algorithm::trim(s);
41 data_.back().second.push_back( Entry( s, string() ) );
42 }
43
44 IniData & data_;
45 };
46
47 struct add_value
48 {
49 add_value( IniData & data ) : data_(data) {}
50
51 void operator ()( char const * p, char const * q) const
52 {
53 data_.back().second.back().second.assign(p, q);
54 }
55
56 IniData & data_;
57 };


イベントハンドラーは、入力として(2つのポインターを介して)文字列の一部を受け取るファンクターです。
パーサーが次のセクションを認識すると、add_sectionファンクターが呼び出されます。 Add_sectionは、このセクションの名前をパラメーターとして取得します。 パーサーが新しいパラメーターの名前を認識すると、add_keyファンクターが呼び出されます。 パーサーがパラメーターの値を認識すると、add_valueファンクターが呼び出されます。 これらのファンクターを使用して、IniDataの順次入力が編成されます。最初に空のセクション(add_section)が追加され、次に空の値を持つエントリ(add_key)がこのセクションに入れられ、この値が入力されます(add_value)。

次に、文法をBackus-Naur表記からC ++に転送します。 このために、特別なクラスinidata_parserが作成されます。
59 struct inidata_parser : public grammar<inidata_parser>
60 {
61 inidata_parser(IniData & data) : data_(data) {}
62
63 template < typename ScannerT>
64 struct definition
65 {
66 rule<ScannerT> inidata, section, entry, ident, value, stringSpaces, spaces;
67
68 rule<ScannerT> const & start() const { return inidata; }
69
70 definition(inidata_parser const & self)
71 {
72 inidata = *section;
73
74 section = ch_p( '[' )
75 >> ident[add_section(self.data_)]
76 >> ch_p( ']' )
77 >> stringSpaces
78 >> ch_p( '\n' )
79 >> spaces
80 >> *(entry);
81
82 entry = ident[add_key(self.data_)]
83 >> stringSpaces
84 >> ch_p( '=' )
85 >> stringSpaces
86 >> value[add_value(self.data_)]
87 >> spaces;
88
89
90 ident = +(alnum_p | chset<>( "-_.,:(){}#@&*|" ) );
91
92 value = *(~ch_p( '\n' ));
93
94 stringSpaces = *blank_p;
95
96 spaces = *space_p;
97 }
98
99 };
100
101 IniData & data_;
102 };

このクラスは、文法全体をカプセル化します。 さらに詳しく調べます。 59行目では、パーサーがcrtpを使用して文法テンプレートクラスを継承していることがわかります。これは、Spiritが正しく機能するために必要です。 パーサーは、コンストラクターで空のIniDataへのリンクを取得して保存します(61)。 パーサー内で、テンプレート構造定義(63-64)を定義する必要があります。 定義構造には、ルールタイプのデータメンバーがあります。これらは、Backus-Naur(66)の形式の文法の各非終端記号のパーサーです。 メインの非端末-inidata(68)へのリンクを返す開始メンバー関数を定義する必要があります。

定義コンストラクターでは、文法を記述します。 文法はC ++でほぼ一字一句書き直されます。 inidataはいくつかのセクションで構成されます(72)-これはアスタリスクで表されます(Kleeneクロージャと同様ですが、左側にアスタリスクが付きます)。 セクションは角かっこで始まります。このために、組み込みのch_pパーサーが使用され、1文字が解析されます。 Backus-Naur表記のコンマの代わりに、>>演算子が使用されます。 式の後の角括弧内に、イベントハンドラファンクタが記述されます(75、82、86)。 左側の「+」記号は「少なくとも1つ」を意味し、「〜」は否定を意味します。 alnum_p-文字と数字の組み込みパーサー。 chset <>は、文字列の任意の文字に一致します(マイナス記号が最初に来ることが重要です。それ以外の場合は、「az」のように間隔文字として認識されます)。 blank_pは行の空白(スペースまたはタブ)に一致し、space_pは空白(ラインフィードとキャリッジリターンを含む)に一致します。

identとidentCharの非端末は、「+」演算子のおかげで1つに統合されていることに注意してください。これは、Backus-Naur表記では不可能だったためです。 そのような指定はありません。

それはすべて文法です。 コメントを削除し、IniDataで値を探す方法を学ぶことは残っています。
コメントを削除するには、特別なファンクターが必要です。
104 struct is_comment{ bool operator ()( string const & s ) const { return s[ 0 ] == '\n' || s[ 0 ] == ';' ; } };

次に、IniDataで検索関数を作成しましょう。
106 struct first_is
107 {
108 first_is(std::string const & s) : s_(s) {}
109
110 template < class Pair >
111 bool operator ()(Pair const & p) const { return p.first == s_; }
112
113 string const & s_;
114 };
115
116 bool find_value( IniData const & ini, string const & s, string const & p, string & res )
117 {
118 IniData::const_iterator sit = find_if(ini.begin(), ini.end(), first_is(s));
119 if (sit == ini.end())
120 return false ;
121
122 Entries::const_iterator it = find_if(sit->second.begin(), sit->second.end(), first_is(p));
123 if (it == sit->second.end())
124 return false ;
125
126 res = it->second;
127 return true ;
128 }

first_isファンクターの代わりに、boost :: bindを適用できますが、1つのヒープ内のすべてに干渉しないことにしました。 検索はすべて簡単です。最初に、名前でセクションを検索し、次にセクションレコードのリストで名前でパラメーターを検索し、すべてが見つかった場合、linkパラメーターを通じてパラメーターの値を返します。

mainを書くことは残っています。
130 int main( int argc, char ** argv)
131 {
132 if ( argc != 4 )
133 {
134 cout << "Usage: " << argv[ 0 ] << " <file.ini> <section> <parameter>" << endl;
135 return 0 ;
136 }
137
138 ifstream in(argv[ 1 ]);
139 if ( !in )
140 {
141 cout << "Can't open file \" " << argv[ 1 ] << '\"' << endl;
142 return 1 ;
143 }
144
145 vector< string > lns;
146
147 std::string s;
148 while ( !in.eof() )
149 {
150 std::getline( in, s );
151 boost::algorithm::trim(s);
152 lns.push_back( s+= '\n' );
153 }
154 lns.erase( remove_if(lns.begin(), lns.end(), is_comment()), lns.end());
155 string text = accumulate( lns.begin(), lns.end(), string() );
156
157 IniData data;
158 inidata_parser parser(data); // Our parser
159 BOOST_SPIRIT_DEBUG_NODE(parser);
160
161 parse_info<> info = parse(text.c_str(), parser, nothing_p);
162 if (!info.hit)
163 {
164 cout << "Parse error \n " ;
165 return 1 ;
166 }
167
168 string res;
169 if (find_value(data, argv[ 2 ], argv[ 3 ], res))
170 cout << res;
171 else
172 cout << "Can't find requested parameter" ;
173 cout << endl;
174 }


132〜136行目-プログラムパラメータを確認します。4つがない場合は、使用状況を表示します。 パラメーターがすべて問題ない場合は、ファイルを開きます(138-143)。 ファイルに問題がなければ、lns行の配列(145)を作成し、ファイル全体(147-153)を読み取ります。 その後、is_comment(154)格納ファンクターを使用して、そこからコメントを削除します。 結論として、すべての行を1つ(155)に接着します。

157-159行目では、パーサーが作成および初期化されます。 次に、パーサーを起動します。これには、テキスト自体、パーサー、およびスキップされた文字用の特別なパーサーを使用する解析関数を使用します(たとえば、すべてのスペースをスキップします)。 この場合、スキップされた文字のパーサーは空になります-nothing_p(つまり、解析なし)。 解析関数の結果は、parse_info <>構造です。 この構造のブールフィールドヒットに興味があります。エラーが発生していない場合はtrueです。 行162〜166では、エラーが発生したかどうかを報告します。 コマンドラインで指定されたパラメーターを見つけ、その値(168-173)を表示するためだけに残ります。

これで、コードは完全に作成されました。 コンパイルして、テスト例で実行します。
$ g++ ini.cpp -o ini_cpp

$ ./ini_cpp /usr/lib/firefox-3.0.5/application.ini App ID
{ec8030f7-c20a-464f-9b0e-13a3a9e97384}

$ ./ini_cpp /usr/lib/firefox-3.0.5/application.ini App IDD
Can't find requested parameter


この記事が、独自のパーサーの作成に役立つことを願っています=)

興味深いメモ:この記事のパーサーと、 「Haskellのiniファイル用のパーサーの作成」の Haskellのパーサーを比較できます

PS。 この記事のC ++ブログへの移植にご協力いただきありがとうございます。

Source: https://habr.com/ru/post/J50976/


All Articles