Boost.Spirit 2 на примере анализа IRC сообщений

На просторах сети есть много статей по данному вопросу, но почему-то 99% из них — это перевод документации на русский язык. Я решил поделиться своим опытом «вызова духов» на, так сказать, реальном примере.
Я являюсь автором Acetamide — это плагин для Leechcraft, который обеспечивает возможность общения по протоколу IRC (rfc 2812).
Boost::Spirit я использовал для разбора входящих сообщений для выделения из них необходимых мне частей.

Формат IRC сообщения, согласно rfc 2812, в форме Бэкуса-Наура выглядит так:

message = [ ":" prefix SPACE ] command [ params ] crlf

Как мы видим, сообщение IRC состоит из 4 частей, из которых обязательная только одна:
  • prefix (необязательная);
  • command (обязательная);
  • params (необязательная)

Начнем:
prefix = servername / ( nickname [ [ "!" user ] "@" host ] ). — Из этой части сообщения нас интересует параметр nickname, который нам необходим для определения отправителя сообщения.

command = 1*letter / 3digit. — этот параметр нас интересует полность. Этот параметр позволяет определить вид действия, которе передает сообщения.
params = *14( SPACE middle ) [ SPACE ":" trailing ] / 14( SPACE middle ) [ SPACE [ ":" ] trailing ]. Этот параметр включает в себя всю остальную часть сообщения. Тут стоит отметить, что в случае наличия последней части, отделяемой ":", то эта часть является сообщением.

Теперь перейдем к тому, из чего состоят вышеперечисленные части:
nospcrlfcl; any octet except NUL, CR, LF, " " and ":"
middle = nospcrlfcl *( ":" / nospcrlfcl )
trailing = *( ":" / " " / nospcrlfcl )
servername = hostname
hostname = shortname *( "." shortname )
shortname = ( letter / digit ) *( letter / digit / "-" ) *( letter / digit )
nickname = ( letter / special ) *8( letter / digit / special / "-" ) — тут стоит отметить, что протокол IRC cильно эволюционировал и во многих местах различается с документацией. Например, в настоящее время отсутствует ограничение на 9 символов ника.

На основании этих данных я составил структутру:

struct IrcMessageStruct
{
 std::string Nickname_; //ник отправителя.
 std::string Command_; // комманда
 QList<std::string> Parameters_; // список параметров
 std::string Message_; // cообщение
}


В эту структуру я и собираюсь сохранять результат разбора входящего сообщения.

Перейдем непосредственно к программированию самого парсере.
В Boost.Spirit 2 рекомендуется использовть для сложных парсеров Grammar — это структура, которая объединяет все правила для определенного действия.

Grammar имеет следующий каркас:
template <typename Iterator>
struct Grammar : qi::grammar<Iterator>
{
	Grammar () 
	: Grammar::base_type (#правило верхнего уровня)
	{
# тут описывается  семантика правил
        }

# тут объявляются сами правила
};
Согласно документации boost'а о сохранении результата разбора в структуру () изменим свой парсер.

Для начала нам необходимо, используя макрос BOOST_FUSION_ADAPT_STRUCT, создать шаблон для нашей структуры:

BOOST_FUSION_ADAPT_STRUCT
(
 IrcMessageStruct,
 (std::string, Nickname_)
 (std::string, Command_)
 (std::list<std::string>, Parameters_)
 (std::string, Message_)
)


Затем изменим наш Grammar для возвращения результата как структуры. Для этого измени вид объявления Grammar на следующий:
template <typename Iterator> struct Grammar : qi::grammar<Iterator, IrcMessageStruct ()>
Создадим правило верхнего уровня, которое собственно и будет записывать результат в структуру:

Объявление:
qi::rule<Iterator, IrcMessageStruct ()> MainRule_; 

второй параметр говорит о том, что результатом работы правила является структура.

Определение:
MainRule_ = -qi::omit [':']
 >> -Nickname_
 >> -qi::omit [Nick_]
 >> -qi::omit [qi::ascii::space]
 >> Command_
 >> -qi::omit [qi::ascii::space]
 >> -Params_
 >> -qi::omit [qi::ascii::space]
 >> -qi::omit [':']
 >> -Msg_;


Cимвол "-" перед правилом или элементом обозначает опциональность. Тоесть элемент может встречаться 0 или 1 раз.
qi::omit [] — данная директива обозначает, что парсеру необходимо игнорировать аттрибут этого типа. Тоесть, при азборе эти параметры не будут рассматриваться как потенциальные значения полей структуры.
qi::ascii::space — этот элемент обозначает символ пробела.
a >> b — этот оператор обозначает, что за a следует b.

Теперь перейдем к реализации следующего правила.

qi::rule<Iterator, std::string ()> Nickname_;

Возвращает в качестве результата разбора строку типа std::string
Nickname_ = qi::raw [ShortName_ % '.'];


% — оператор списка, который эквивалентен следующей записи:

ShortName_ >> *('.' >> ShortName_ )


qi::rule<Iterator, std::string ()> Nick_;
Nick_ = -(-(qi::ascii::char_ ('!')
		>> User_)
		>> qi::ascii::char_ ('@')
		>> Host_);

Далее все правила реализуются по тому же алгоритму.

Остановлюсь на таком моменте, как создание перечня символов:
для BNF special; "[", "]", "\", "`", "_", "^", "{", "|", "}"
Реализуется следующим правилом:
qi::rule<Iterator> Special_;
Special_ = qi::ascii::char_ ("[]\\`_^{|}");

Так же любопытен момент обратный — исключение из перечня символов:
BNF: user; any octet except NUL, CR, LF, " " and "@"
Реализуется следующим правилом:
qi::rule<Iterator, std::string ()> User_;
User_ = +(qi::ascii::char_ - '\r' - '\n' - ' ' - '@' - '\0');

C этим ничего сложного нету. Наибольшие трудности у меня вызвал способ парсинга в поле структуры, которое является списком. Способы использования SemanticActions мне не подходили, потому что тогда переставали записываться значания в остальные поля структуры. В итоге решение выглядит следующим образом.
Для начала создаем правило, которое в качестве результата возвращает нам список:
qi::rule<Iterator, std::list<std::string> ()> Params_;

Определяем это правило как:
Params_ = FirstParameters_ % -qi::ascii::space;

И так, как элемент нашего списка std::string, то и правило FirstParameters_ должно возвращать тип std::string:
qi::rule<Iterator, std::string ()> FirstParameters_;

FirstParameters_ = qi::raw [Nospcrlfcl_ >> *(qi::ascii::char_ (':') | Nospcrlfcl_)];

Таким образом результат записывается в QList<std::string>. По созданю парсера, как мне кажется, все понятно.

Теперь каким образом вызывать эту грамматику:
Создаем объект Grammar:
Grammar<std::string::const_iterator> g;


Создаем объект структуры:
IrcMessageStruct ims;


вызываем функцию парсинга:
std::string::const_iterator first = str.begin();
std::string::const_iterator last = str.end();
qi::parse (first, last, g, ims);

Необходимо так же проверить на то, что сообщение было разобрано полностью, иначе парсер вернет положительный результат для разобранной части. Для этого проверим значения итераторов:
if (first == last)


В итоге вызов парсера выглядит следующим образом:
bool r = qi::parse (first, last, g, ims) && (first == last);


Так же хотел бы обратит внимние на такую очень полезную вещь как debug ():
Эта функция позволяет отслеживать результат выполнения парсинга в формате xml.
Для удобства отображения задаем для каждого правила имя:
Nick_.name ("nick");

и вызываем:
debug (Nick_);


Ссылка на полный код парсера. На вход подается строка, а не файл.:
Ссылка на файл с несколькими видами входящих сообщений:

P.S. если у кого-то будут предложения по улучшению моего парсера — то я с радостью их выслушаю, потому что это мое первое знакомство с Boost.Spirit 2.


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.