boost::spirit::qi解析器空行解析与错误行号检测问题
问题描述
我需要升级一个解析器以扩展其功能。
该解析器用于读取脚本文件中的所有行,将全部内容(包括空行和#注释)存入std::vector,要求原文件的行数与vector的大小完全相等。
此外,如果脚本存在错误,解析器需要停止解析,并存储错误出现的行号(下方示例中暂未实现该功能)。
遇到的问题
- 我无法编写出解析空行的规则,空行可以是完全空的行,也可以是仅包含空格和/或制表符的行。我已经尝试过
qi::eps等方案,但仍然无法实现该功能。我猜测可能和规则中使用qi::blank_type作为跳过器有关,但暂不能确定。 - 错误行号检测可以通过多种方式实现:解析完成后统计文件行数和vector大小比对(该方案过于简陋,已排除)、使用on_error从句将所有序列规则(a >> b)替换为期望规则(a > b),我想知道是否有其他更好的方法可以定位错误行号?
原始代码
#include <boost/spirit/include/qi.hpp> #include <boost/phoenix/phoenix.hpp> namespace qi = boost::spirit::qi; enum class TYPE { NONE, LOG, END, JUMP_TO, WAIT_TIME, SEND, COMMENT, LABEL }; const char* names[]{ "NONE", "LOG", "END", "JUMP_TO", "WAIT_TIME", "SEND", "COMMENT", "LABEL" }; struct Command { TYPE type; std::string arg1; std::string arg2; }; typedef std::vector<Command> Commands; BOOST_FUSION_ADAPT_STRUCT(Command, type, arg1, arg2) template <typename It> class Parser : public qi::grammar<It, Commands()> { private: qi::rule<It> none; qi::rule<It, Command(), qi::blank_type> log; qi::rule<It, Command(), qi::blank_type> end; qi::rule<It, Command(), qi::blank_type> jump_to; qi::rule<It, Command(), qi::blank_type> wait_time; qi::rule<It, Command(), qi::blank_type> send; qi::rule<It, Command(), qi::blank_type> comment; qi::rule<It, Command(), qi::blank_type> label;//By its very nature, "label" must be the last command to be checked qi::rule<It, Commands()> start; public: Parser() : Parser::base_type(start) { using namespace qi; //none = *~char_("\r\n"); // 'none' rule should parse for blank lines, but I can not figure out how. log = lit("LOG") >> '(' >> attr(TYPE::LOG) >> lexeme[+~char_(")\r\n")] >> ')' >> attr(std::string{});//ignore arg2 end = lit("END") >> '(' >> attr(TYPE::END) >> raw[double_] >> ')'//NOTE: "as_string[raw[double_]]" is also valid >> attr(std::string{});//ignore arg2 jump_to = lit("JUMP_TO") >> '(' >> attr(TYPE::JUMP_TO) >> lexeme[+~char_(")\r\n")] >> ')' >> attr(std::string{});//ignore arg2 wait_time = lit("WAIT_TIME") >> '(' >> attr(TYPE::WAIT_TIME) >> raw[double_] >> ')'//NOTE: "as_string[raw[double_]]" is also valid >> attr(std::string{});//ignore arg2 send = lit("SEND") >> '(' >> attr(TYPE::SEND) >> lexeme[+~char_(",)\r\n")] >> ',' >> +xdigit >> ')'; comment = lit("#") >> attr(TYPE::COMMENT) >> lexeme[+~char_("\r\n")] >> attr(std::string{});//ignore arg2 label = attr(TYPE::LABEL) >> lexeme[+~char_(": \r\n")] >> ':' >> attr(std::string{});//ignore arg2 start = skip(blank)[(log | end | jump_to | wait_time | send | comment | label | none) % eol]; on_error<fail> ( start, boost::phoenix::ref(std::cout) << "Error detected" << std::endl ); } }; Commands parse(std::istream& in) { using It = boost::spirit::istream_iterator; static const Parser<It> parser; Commands commands; It first(in >> std::noskipws), last;//No white space skipping if (!qi::parse(first, last, parser, commands)) throw std::runtime_error("command parse error"); return commands; } int main() { std::stringstream test1; test1 << "JUMP_TO(etiqueta)" << '\n' << "LOG(this is to be writen in the log)" << '\n' << "SEND(id_8, AF9E02CA7EFF)" << '\n' << "LOG(write me in a log 1)" << '\n' << "LOG(write me in a log 2" << '\n' //On purpose error!!! Missed second parenthesis ')' << "END(5.75)" << '\n' << "LABEL1:" << '\n' << '\n' << "#On the fly comment" << '\n' << "WAIT_TIME(25)" << '\n' << "SEND(id_3, AF9E02CA7EFF)" << '\n'; std::stringstream test2; test2 << "JUMP_TO(etiqueta)" << '\n' << "LOG(this is to write in the log)" << '\n' << "SEND(id_8, AF9E02CA7EFF)" << '\n' << "LOG(write me in a log 1)" << '\n' << "LOG(write me in a log 2)" << '\n' << "END(5.75)" << '\n' << "xxxxx non sense xxxxxx" << '\n' //On purpose error!!! Nonsense sentence << "LABEL1:" << '\n' << '\n' << "#On the fly comment" << '\n' << "WAIT_TIME(25)" << '\n' << "SEND(id_3, AF9E02CA7EFF)" << '\n'; std::stringstream test3; test3 << "JUMP_TO(etiqueta)" << '\n' << "LOG(this is to write in the log)" << '\n' << "SEND(id_8, AF9E02CA7EFF)" << '\n' << "LOG(write me in a log 1)" << '\n' << "LOG(write me in a log 2)" << '\n' << "END(5.75)" << '\n' << '\n' //This is not an error, but a permitted blank line!!! It should be parses as NONE type << "LABEL1:" << '\n' << '\n' << "#On the fly comment" << '\n' << "WAIT_TIME(25)" << '\n' << "SEND(id_8, AF9E02CA7EFF)" << '\n'; try { auto commands1 = parse(test1); std::cout << "Test1:\n"; for (auto& cmd : commands1) std::cout << names[static_cast<int>(cmd.type)] << '\t' << cmd.arg1 << '\t' << cmd.arg2 << std::endl;//vector size must be 4 (4 lines in console) - Ok std::cout << "\nTest2:\n"; auto commands2 = parse(test2); for (auto& cmd : commands2) std::cout << names[static_cast<int>(cmd.type)] << '\t' << cmd.arg1 << '\t' << cmd.arg2 << std::endl;//vector size must be 6 (6 lines in console) - Ok std::cout << "\nTest3:\n"; auto commands3 = parse(test3); for (auto& cmd : commands3) std::cout << names[static_cast<int>(cmd.type)] << '\t' << cmd.arg1 << '\t' << cmd.arg2 << std::endl;//vector size must be 12 (12 lines in console) - Wrong } catch (std::exception const& e) { std::cout << e.what() << "\n"; } }
解决方案
1. 空行解析修复
你的猜测是正确的,问题出在两个点:
- 原有
none规则没有指定正确的跳过器和返回属性,无法生成Command对象存入vector - 空行匹配逻辑没有适配当前的
skip(blank)上下文
修改方案:
首先修改none规则的定义:
qi::rule<It, Command(), qi::blank_type> none; // 补充跳过器和返回属性
然后在规则实现部分填写:
none = attr(TYPE::NONE) >> attr(std::string{}) >> attr(std::string{});
因为在skip(blank)的上下文中,行首的所有空格/制表符都会被自动跳过,空行场景下没有其他有效字符,none分支就会被命中,自动生成TYPE为NONE的Command对象,保证vector大小和行数一致。
另外可以优化start规则增加完整性校验,避免末尾残留字符未被解析:
start = skip(blank)[(log | end | jump_to | wait_time | send | comment | label | none) % eol] >> -eol >> eoi;
2. 错误行号定位
直接使用Boost.Spirit提供的line_pos_iterator包装输入迭代器,无需手动计数即可获取错误行号:
首先修改parse函数的迭代器定义:
using It = boost::spirit::line_pos_iterator<boost::spirit::istream_iterator>;
对应修改迭代器初始化逻辑:
It first(boost::spirit::istream_iterator(in >> std::noskipws)), last;
然后在on_error回调中直接获取行号:
on_error<fail> ( start, boost::phoenix::ref(std::cout) << "Error detected at line: " << get_line(_1) // _1是错误位置的迭代器,get_line直接返回行号 << std::endl );
如果需要更精准的错误定位,可以把规则中必须连续匹配的部分从>>替换为期望操作符>,匹配失败时不会回溯,直接触发错误。
修改后test3的vector大小会符合预期的12行,错误场景也能直接输出对应的错误行号。
内容的提问来源于stack exchange,提问作者Pablo
相关产品推荐
相关产品推荐

