boost::spirit::qi 解析器中的空行解析和错误检测

Parsing blank lines and error detection in boost::spirit::qi parser

我需要更新解析器以提供额外的功能。

解析器将用于读取脚本文件中给定的所有行,并将所有行(甚至空白行和#comments)复制到std::vector, 所以原始文件的行数和向量的大小应该是相等的。

此外,如果脚本包含错误,解析器将停止解析,并存储出现错误的行号(在下面的示例中未实现)。

我的问题是:

1/ 我无法制定规则来解析空行(空行可能是空行或仅包含空格 and/or 制表符)。我尝试了 qi::eps 和其他一些尝试,但仍然无法实现。可能与qi::blank_type在规则中用作船长有关,但不确定。

2/检测错误行,如果有的话,可以通过不同的方式来完成:计算文件中解析后的行数并与向量的大小进行比较(这个真的很幼稚,放弃它),使用 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";
    }
}

及其大肠杆菌link:http://coliru.stacked-crooked.com/a/c4830afc26371712

任何帮助将不胜感激。

首先,为了保持线路同步,您还应该 none 构建一个命令属性。

二、匹配所有空格:

none = *blank

但这会尝试将向量分配给 TYPE 字段,因此我们告诉它省略属性:

none = omit[*blank]

现在可能会读取零个或多个空格,但不能证明我们在行尾,让我们断言:

none = omit[*blank] >> &eol

但是如果最后一行是空行呢? % eol 这不是问题,但为了 clean/defensive 代码的利益:

none = omit[*blank] >> &(eol | eoi)

现在,让我们确保结果是一个没有参数的 NONE 命令:

none = omit[*blank] >> &(eol | eoi) //
        >> attr(TYPE::NONE) >> stub_arg >> stub_arg;

这里我把 attr(string{}) 东西藏在一个更漂亮的名字下:

auto stub_arg = copy(attr(std::string{}));

现在我们有一个可以正常工作的演示:

//#define BOOST_SPIRIT_DEBUG
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/phoenix/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iomanip>
#include <iostream>
#include <sstream>

namespace qi = boost::spirit::qi;

enum class TYPE {
    NONE,
    LOG,
    END,
    JUMP_TO,
    WAIT_TIME,
    SEND,
    COMMENT,
    LABEL
};

static inline std::ostream& operator<<(std::ostream& os, TYPE t) {
    static constexpr std::array names{"NONE",      "LOG",  "END",     "JUMP_TO",
                                      "WAIT_TIME", "SEND", "COMMENT", "LABEL"};
    return os << names.at(static_cast<int>(t));
}

struct Command {
    TYPE        type = TYPE::NONE;
    std::string arg1, arg2;
};

using Commands = std::vector<Command>;

BOOST_FUSION_ADAPT_STRUCT(Command, type, arg1, arg2)

template <typename It>
class Parser : public qi::grammar<It, Commands()>
{
  private:
    qi::rule<It, Command(), qi::blank_type> none, //
        log, end, jump_to, wait_time, send, comment, label;
    qi::rule<It, Commands()> start;

  public:
    Parser() : Parser::base_type(start)
    {
        using namespace qi;
        auto stub_arg = copy(attr(std::string{}));

        log = lit("LOG") >> '('                 //
            >> attr(TYPE::LOG)                  //
            >> lexeme[+~char_(")\r\n")] >> ')'  //
            >> stub_arg;                        //
        end = lit("END") >> '('                 //
            >> attr(TYPE::END)                  //
            >> raw[double_] >> ')'              //
            >> stub_arg;                        //
        jump_to = lit("JUMP_TO") >> '('         //
            >> attr(TYPE::JUMP_TO)              //
            >> lexeme[+~char_(")\r\n")] >> ')'  //
            >> stub_arg;                        //
        wait_time = lit("WAIT_TIME") >> '('     //
            >> attr(TYPE::WAIT_TIME)            //
            >> raw[double_] >> ')'              //
            >> stub_arg;                        //
        send = lit("SEND") >> '('               //
            >> attr(TYPE::SEND)                 //
            >> lexeme[+~char_(",)\r\n")] >> ',' //
            >> +xdigit >> ')';
        comment = lit("#")                      //
            >> attr(TYPE::COMMENT)              //
            >> lexeme[+~char_("\r\n")]          //
            >> stub_arg;                        //
        label = attr(TYPE::LABEL)               //
            >> lexeme[+~char_(": \r\n")] >> ':' //
            >> stub_arg;                        //

        none = omit[*blank] >> &(eol | eoi) //
            >> attr(TYPE::NONE) >> stub_arg >> stub_arg;

        start =
            skip(blank)[ //
                (log | end | jump_to | wait_time | send | comment | label | none
                 ) %
                eol];

        BOOST_SPIRIT_DEBUG_NODES(
            (none)(log)(end)(jump_to)(wait_time)(send)(comment)(label)(start))
    }
};

Commands parse(std::string text)
{
    std::istringstream in(std::move(text));
    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()
{
    for (std::string const test : {
             R"(JUMP_TO(etiqueta)
                LOG(this is to be writen in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2
                END(5.75)
                LABEL1:

                #On the fly comment
                WAIT_TIME(25)
                SEND(id_3, AF9E02CA7EFF))",
             R"(JUMP_TO(etiqueta)
                LOG(this is to write in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2)
                END(5.75)
                xxxxx non sense xxxxxx
                LABEL1:

                #On the fly comment
                WAIT_TIME(25)
                SEND(id_3, AF9E02CA7EFF))",
             R"(JUMP_TO(etiqueta)
                LOG(this is to write in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2)
                END(5.75)
                
                LABEL1:

                #On the fly comment
                WAIT_TIME(25)
                SEND(id_8, AF9E02CA7EFF))"})
        try {
            std::cout << "================================\n";
            std::vector<std::string_view> lines;
            boost::algorithm::split(
                lines, test, boost::algorithm::is_any_of("\n"));
            auto commands = parse(test);

            for (size_t i = 0;
                 i < std::min(std::size(commands), std::size(lines)); ++i) //
            {
                std::cout << "#" << std::left << std::setw(4) << i
                          << " " << std::quoted(lines[i]) << "\n";

                auto& cmd = commands[i];
                std::cout << std::setw(6) << " -> " << cmd.type;

                std::cout << "(" //
                          << std::quoted(cmd.arg1) << ", "
                          << std::quoted(cmd.arg2) << ")" << std::endl;
            }

            for (size_t i = std::size(commands); i < std::size(lines); ++i) //
            {
                std::cout << "#" << std::left << std::setw(4) << i
                          << " " << std::quoted(lines[i]) << "\n";
            }
        } catch (std::exception const& e) {
            std::cout << e.what() << "\n";
        }
}

打印(在 Wandbox and Compiler Explorer 上直播):

================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to be writen in the log)"
 ->   LOG("this is to be writen in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2"
#5    "                END(5.75)"
#6    "                LABEL1:"
#7    ""
#8    "                #On the fly comment"
#9    "                WAIT_TIME(25)"
#10   "                SEND(id_3, AF9E02CA7EFF)"
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to write in the log)"
 ->   LOG("this is to write in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2)"
 ->   LOG("write me in a log 2", "")
#5    "                END(5.75)"
 ->   END("5.75", "")
#6    "                xxxxx non sense xxxxxx"
#7    "                LABEL1:"
#8    ""
#9    "                #On the fly comment"
#10   "                WAIT_TIME(25)"
#11   "                SEND(id_3, AF9E02CA7EFF)"
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to write in the log)"
 ->   LOG("this is to write in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2)"
 ->   LOG("write me in a log 2", "")
#5    "                END(5.75)"
 ->   END("5.75", "")
#6    "                "
 ->   NONE("", "")
#7    "                LABEL1:"
 ->   LABEL("LABEL1", "")
#8    ""
 ->   NONE("", "")
#9    "                #On the fly comment"
 ->   COMMENT("On the fly comment", "")
#10   "                WAIT_TIME(25)"
 ->   WAIT_TIME("25", "")
#11   "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")

其他内容

我会简化很多。此外,添加一个“包罗万象”的处理程序来标记错误行可能会很好,比如

none = omit[*blank] >> &(eol | eoi) //
    >> attr(TYPE::NONE) >> stub_arg >> stub_arg;
fail = omit[*~char_("\r\n")] //
    >> attr(TYPE::_SYN_ERR) >> stub_arg >> stub_arg;

那么你可以获得:Live On Compiler Explorer/Wandbox

================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to be writen in the log)"
 ->   LOG("this is to be writen in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2"
 ->   #SYNTAX
#5    "                END(5.75)"
 ->   END("5.75", "")
#6    "                LABEL1:"
 ->   LABEL("LABEL1", "")
#7    ""
 ->   NONE("", "")
#8    "                #On the fly comment"
 ->   COMMENT("On the fly comment", "")
#9    "                WAIT_TIME(25)"
 ->   WAIT_TIME("25", "")
#10   "                SEND(id_3, AF9E02CA7EFF)"
 ->   SEND("id_3", "AF9E02CA7EFF")
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to write in the log)"
 ->   LOG("this is to write in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2)"
 ->   LOG("write me in a log 2", "")
#5    "                END(5.75)"
 ->   END("5.75", "")
#6    "                xxxxx non sense xxxxxx"
 ->   #SYNTAX
#7    "                LABEL1:"
 ->   LABEL("LABEL1", "")
#8    ""
 ->   NONE("", "")
#9    "                #On the fly comment"
 ->   COMMENT("On the fly comment", "")
#10   "                WAIT_TIME(25)"
 ->   WAIT_TIME("25", "")
#11   "                SEND(id_3, AF9E02CA7EFF)"
 ->   SEND("id_3", "AF9E02CA7EFF")
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO("etiqueta", "")
#1    "                LOG(this is to write in the log)"
 ->   LOG("this is to write in the log", "")
#2    "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")
#3    "                LOG(write me in a log 1)"
 ->   LOG("write me in a log 1", "")
#4    "                LOG(write me in a log 2)"
 ->   LOG("write me in a log 2", "")
#5    "                END(5.75)"
 ->   END("5.75", "")
#6    "                "
 ->   NONE("", "")
#7    "                LABEL1:"
 ->   LABEL("LABEL1", "")
#8    "                \"\n\""
 ->   #SYNTAX
#9    "                #On the fly comment"
 ->   COMMENT("On the fly comment", "")
#10   "                WAIT_TIME(25)"
 ->   WAIT_TIME("25", "")
#11   "                SEND(id_8, AF9E02CA7EFF)"
 ->   SEND("id_8", "AF9E02CA7EFF")

奖金

这是我对上面的“简化”的看法:

    calltype_.add                           //
        ("LOG", Ast::Call::LOG)             //
        ("END", Ast::Call::LOG)             //
        ("WAIT_TIME", Ast::Call::WAIT_TIME) //
        ("SEND", Ast::Call::SEND)           //
        ("JUMP_TO", Ast::Call::JUMP_TO)     //
        ;

    call    = calltype_ >> '(' >> arg % ',' >> ')';
    arg     = +~char_("(),\r\n");
    comment = "#" >> +~char_("\r\n");
    empty   = &(eol | eoi);
    invalid = omit[*~char_("\r\n")] >> attr(Ast::Invalid{});
    label   = +~char_(": \r\n") >> ':';

    start = skip(blank)[(call | comment | label | empty | invalid) % eol] >>
        eoi;

这就是整个语法。现在的输出是

split(lines, test, boost::algorithm::is_any_of("\n"));
auto script = parse(test);

for (size_t i = 0; i < std::size(lines); ++i) {
    boost::algorithm::trim_copy(lines[i]);
    std::cout << "#" << std::left << std::setw(4) << i << " "
              << std::quoted(trim_copy(lines[i])) << "\n";

    if (i < script.size()) {
        std::cout << std::setw(6) << " -> " << script[i] << "\n";
    }
}

完整现场演示:现场直播 Wandbox/Compiler Explorer

#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/phoenix/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iomanip>
#include <iostream>
#include <sstream>

using boost::algorithm::split;
using boost::algorithm::trim_copy;
namespace qi = boost::spirit::qi;

namespace Ast {
    using Arg  = std::string;
    using Args = std::vector<Arg>;
    struct Empty { };
    struct Label { std::string name; };
    struct Comment { std::string text; };
    struct Invalid { };

    struct Call {
        enum Type { LOG, END, JUMP_TO, WAIT_TIME, SEND };
        Type type;
        Args args;

        bool is_valid() const
        {
            switch (type) {
                case Type::LOG:
                case Type::END:
                case Type::JUMP_TO:
                case Type::WAIT_TIME: return args.size() == 1;
                case Type::SEND: return args.size() == 2;
            }
            return false;
        }

        friend inline std::ostream& operator<<(std::ostream& os, Type t) {
            static constexpr std::array names{
                "LOG", "END", "JUMP_TO", "WAIT_TIME", "SEND",
            };
            return os << names.at(static_cast<int>(t));
        }

        friend inline std::ostream& operator<<(std::ostream& os, Call const& c) {
            os << c.type << "(";
            bool first = true;
            for (auto& arg : c.args)
                os << (std::exchange(first, false) ? "" : ",") << arg;
            return os << ")";
        }
    };

    using Line   = boost::variant<Empty, Call, Label, Comment, Invalid>;
    using Script = std::vector<Line>;

    static inline std::ostream& operator<<(std::ostream& os, Invalid)          { return os << "#INVALID COMMAND LINE"; } 
    static inline std::ostream& operator<<(std::ostream& os, Empty)            { return os << "(empty line)"; } 
    static inline std::ostream& operator<<(std::ostream& os, Label const& l)   { return os << l.name << ":";  } 
    static inline std::ostream& operator<<(std::ostream& os, Comment const& c) { return os << "#" << c.text;  } 
} // namespace Ast

BOOST_FUSION_ADAPT_STRUCT(Ast::Call, type, args)
BOOST_FUSION_ADAPT_STRUCT(Ast::Label, name)
BOOST_FUSION_ADAPT_STRUCT(Ast::Comment, text)

template <typename It>
class Parser : public qi::grammar<It, Ast::Script()>
{
  public:
    Parser() : Parser::base_type(start)
    {
        using namespace qi;

        calltype_.add                           //
            ("LOG", Ast::Call::LOG)             //
            ("END", Ast::Call::LOG)             //
            ("WAIT_TIME", Ast::Call::WAIT_TIME) //
            ("SEND", Ast::Call::SEND)           //
            ("JUMP_TO", Ast::Call::JUMP_TO)     //
            ;

        call    = calltype_ >> '(' >> arg % ',' >> ')';
        arg     = +~char_("(),\r\n");
        comment = "#" >> +~char_("\r\n");
        empty   = &(eol | eoi);
        invalid = omit[*~char_("\r\n")] >> attr(Ast::Invalid{});
        label   = +~char_(": \r\n") >> ':';

        start = skip(blank)[(call | comment | label | empty | invalid) % eol] >>
            eoi;

        BOOST_SPIRIT_DEBUG_NODES((empty)(call)(arg)(comment)(label)(start)(invalid))
    }

  private:
    qi::symbols<char, Ast::Call::Type>        calltype_;
    qi::rule<It, Ast::Call(), qi::blank_type> call;
    qi::rule<It, Ast::Script()>               start;

    qi::rule<It, Ast::Arg()>     arg;
    qi::rule<It, Ast::Comment()> comment;
    qi::rule<It, Ast::Empty()>   empty;
    qi::rule<It, Ast::Invalid()> invalid;
    qi::rule<It, Ast::Label()>   label;
};

Ast::Script parse(std::string text)
{
    std::istringstream in(std::move(text));
    using It = boost::spirit::istream_iterator;

    static const Parser<It> parser;

    Ast::Script 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()
{
    for (std::string const& test : {
             R"(JUMP_TO(etiqueta)
                LOG(this is to be writen in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2
                END(5.75)
                LABEL1:

                #On the fly comment
                WAIT_TIME(25)
                SEND(id_3, AF9E02CA7EFF))",
             R"(JUMP_TO(etiqueta)
                LOG(this is to write in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2)
                END(5.75)
                xxxxx non sense xxxxxx
                LABEL1:

                #On the fly comment
                WAIT_TIME(25)
                SEND(id_3, AF9E02CA7EFF))",
             R"(JUMP_TO(etiqueta)
                LOG(this is to write in the log)
                SEND(id_8, AF9E02CA7EFF)
                LOG(write me in a log 1)
                LOG(write me in a log 2)
                END(5.75)
                
                LABEL1:
                "\n"
                #On the fly comment
                WAIT_TIME(25)
                SEND(id_8, AF9E02CA7EFF))"})
        try {
            std::cout << "================================\n";
            std::vector<std::string_view> lines;
            split(lines, test, boost::algorithm::is_any_of("\n"));
            auto script = parse(test);

            for (size_t i = 0; i < std::size(lines); ++i) {
                std::cout << "#" << std::left << std::setw(4) << i << " "
                          << std::quoted(trim_copy(lines[i])) << "\n";

                if (i < script.size()) {
                    std::cout << std::setw(6) << " -> " << script[i] << "\n";
                }
            }
        } catch (std::exception const& e) {
            std::cout << e.what() << "\n";
        }
}

打印

================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO(etiqueta)
#1    "LOG(this is to be writen in the log)"
 ->   LOG(this is to be writen in the log)
#2    "SEND(id_8, AF9E02CA7EFF)"
 ->   SEND(id_8,AF9E02CA7EFF)
#3    "LOG(write me in a log 1)"
 ->   LOG(write me in a log 1)
#4    "LOG(write me in a log 2"
 ->   #INVALID COMMAND LINE
#5    "END(5.75)"
 ->   LOG(5.75)
#6    "LABEL1:"
 ->   LABEL1:
#7    ""
 ->   (empty line)
#8    "#On the fly comment"
 ->   #On the fly comment
#9    "WAIT_TIME(25)"
 ->   WAIT_TIME(25)
#10   "SEND(id_3, AF9E02CA7EFF)"
 ->   SEND(id_3,AF9E02CA7EFF)
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO(etiqueta)
#1    "LOG(this is to write in the log)"
 ->   LOG(this is to write in the log)
#2    "SEND(id_8, AF9E02CA7EFF)"
 ->   SEND(id_8,AF9E02CA7EFF)
#3    "LOG(write me in a log 1)"
 ->   LOG(write me in a log 1)
#4    "LOG(write me in a log 2)"
 ->   LOG(write me in a log 2)
#5    "END(5.75)"
 ->   LOG(5.75)
#6    "xxxxx non sense xxxxxx"
 ->   #INVALID COMMAND LINE
#7    "LABEL1:"
 ->   LABEL1:
#8    ""
 ->   (empty line)
#9    "#On the fly comment"
 ->   #On the fly comment
#10   "WAIT_TIME(25)"
 ->   WAIT_TIME(25)
#11   "SEND(id_3, AF9E02CA7EFF)"
 ->   SEND(id_3,AF9E02CA7EFF)
================================
#0    "JUMP_TO(etiqueta)"
 ->   JUMP_TO(etiqueta)
#1    "LOG(this is to write in the log)"
 ->   LOG(this is to write in the log)
#2    "SEND(id_8, AF9E02CA7EFF)"
 ->   SEND(id_8,AF9E02CA7EFF)
#3    "LOG(write me in a log 1)"
 ->   LOG(write me in a log 1)
#4    "LOG(write me in a log 2)"
 ->   LOG(write me in a log 2)
#5    "END(5.75)"
 ->   LOG(5.75)
#6    ""
 ->   (empty line)
#7    "LABEL1:"
 ->   LABEL1:
#8    "\"\n\""
 ->   #INVALID COMMAND LINE
#9    "#On the fly comment"
 ->   #On the fly comment
#10   "WAIT_TIME(25)"
 ->   WAIT_TIME(25)
#11   "SEND(id_8, AF9E02CA7EFF)"
 ->   SEND(id_8,AF9E02CA7EFF)