Pull to refresh

Создаём парсер для ini-файлов на C++

Reading time7 min
Views38K
В данной статье я расскажу как написать свой парсер ini-файлов на C++. За основу возьмём контекстно-свободную грамматику, построенную в моей предыдущей статье. Для построения парсера будет использоваться библиотека 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[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, но я решил не мешать всё в одну кучу. С поиском всё просто: сначала в списке ищем секцию по имени, потом в списке записей секции ищем параметр по имени, и, если всё нашлось, то возвращаем значение параметра через параметр-ссылку.

Осталось написать 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, то выводим 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


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

Интересное замечание: вы можете сравнить парсер из этой статьи с парсером на Haskell из статьи «Создаём парсер для ini-файлов на Haskell».

PS. Спасибо, что помогли перенести эту статью в блог C++.
Tags:
Hubs:
+48
Comments43

Articles