You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 15:48:05