1 апр. 2009 г.

Использование boost::spirit

В данной статье я расскажу как написать свой парсер ini-файлов на C++. За основу возьмём контекстно-свободную грамматику, построенную в предыдущей статье "Создаём парсер для ini-файлов. Теория". Для построения парсера будет использоваться библиотека Boost Spirit, которая позволяет строить свои собственные парсеры комбинируя готовые примитивные парсеры при помощи парсерных комбинаторов.

Важно: в данной статье предполагается, что читатель знаком с основами C++ (в том числе будет активно использоваться STL). Если вы не очень в себе уверены, то я советую сначала прочитать пару статей для новичков по С++ и по 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



Начните с установки boost (можно взять на официальном сайте или поискать готовые пакеты для вашей OS). Собирать boost не требуется, так как весь Spirit живёт в хедерах. Процесс установки для разных систем может быть различным, поэтому я не буду его здесь описывать.

Я постараюсь подробно описать процесс создания парсера на С++. При этом я не буду особенно думать о производительности, так как это не является целью данной статьи.

Начнём с подключения необходимых хедеров.
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). Конструкция using namespace не всегда является хорошей практикой, но здесь для краткости я себе это позволю.

Определим типы данных: запись — это пара «ключ — значение», секция — это пара «ключ — список записей», все данные 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 };


Обработчики событий представляют собой функторы, которые принимают на вход кусок строки (через два указателя).
Функтор add_section будет вызываться в тот момент, когда парсер распознает очередную секцию. В качестве параметра add_section получит имя этой секции. Функтор add_key будет вызван в тот момент, когда парсер распознает имя нового параметра. Функтор add_value будет вызван в тот момент, когда парсер распознает значение параметра. При помощи этих функторов организуется последовательное заполнение IniData: сначала добавляется пустая секция (add_section), потом в эту секцию кладется Entry с незаполненным значением (add_key), а потом это значение заполняется (add_value).

Теперь будем переносить грамматику из нотации Бэкуса-Наура в 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 мы видим, что парсер наследуется от шаблонного класса grammar, используя crtp, — это необходимo для правильной работы Spirit-а. Парсер принимает в конструкторе ссылку на незаполненную IniData и сохраняет её (61). Внутри парсера нужно определить шаблонную структуру definition (63-64). У структуры definitionесть члены данных типа rule — это парсеры для каждого из нетерминалов нашей грамматики в форме Бэкуса-Наура (66). Необходимо определить функцию-член start, которая будет возвращать ссылку на главный нетерминал — inidata (68).

В конструкторе definition мы описываем грамматику. Грамматика переписывается на C++ почти дословно. inidata состоит из нескольких секций (72) — это выражается звёздочкой (как замыкание Клини, но звёздочка слева). Секция начинается с квадратной скобки — для этого используется встроенный парсер ch_p, который парсит один символ. Вместо запятой из нотации Бэкуса-Наура используется оператор >>. В квадратных скобках после выражения пишется функтор-обработчик события (75, 82, 86). Символ "+" слева означает «хотя бы один», а "~" означает отрицание. alnum_p — встроенный парсер для букв и цифр. chset<> соответствует любому символу из строки (важно, что минус идёт первым, иначе он воспринимается как знак интервала, вроде «a-z»). blank_p соответствует пробельному символу в строке (пробел или табуляция), space_p соответствует любому пробельному символу (в т.ч. и переводу строки и возврату каретки).

Отметим, что нетерминалы ident и identChar удалось слить в один благодаря оператору "+" — в нотации Бэкуса-Наура это было невозможно, т.к. там отсутствует подобное обозначение.

С грамматикой всё. Осталось научиться удалять комментарии и искать значение в IniData.
Для удаления комментариев нам потребуется специальный функтор.
104 struct is_comment{ bool operator()( string const& s ) const { return s[] == '\n' || s[] == ';'; } };

Теперь напишем функцию поиска в 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, но я решил не мешать всё в одну кучу. С поиском всё просто: сначала в списке ищем секцию по имени, потом в списке записей секции ищем параметр по имени, и, если всё нашлось, то возвращаем значение параметра через параметр-ссылку.

Осталось написать main.
130 int main( int argc, char** argv)
131 {
132 if ( argc != 4 )
133 {
134 cout << "Usage: " << argv[] << " <file.ini> <section> <parameter>" << endl;
135 return ;
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, то выводим usage. Если с параметрами всё ок, то открываем файл (138-143). Если и с файлом всё нормально, то создаём массив строк lns (145) и считываем в него весь файл (147-153). После этого удаляем оттуда комментарии, используя припасённый функтор is_comment (154). В заключение склеиваем все строчки в одну (155).

В строчках 157-159 создаётся и инициализируется парсер. Теперь запускаем парсер — для этого используется функция parse, которая принимает на вход сам текст, парсер и специальный парсер для пропускаемых символов (скажем, мы хотели бы пропускать все пробелы). В нашем случае парсер для пропускаемых символов будет пустым — nothing_p (т.е. ничего не парсящий). Результатом функции parse является структура parse_info<>. Нас интересует булево поле hit этой структуры, которое истинное, если не произошло ошибок. В строчках 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


Надеюсь, что данная статья поможет вам написать свой собственный парсер =)


P.S.: Наглым образом спёрто отсюда: Создаём парсер для ini-файлов на C++

4 комментария: