Boost Spirit Qi 验证输入解析器

3

我有一个非常基本的Boost Spirit Qi语法,用于解析IP端口或IP端口范围,即"6322""6322-6325"

该语法如下:

  template<class It>
  void init_port_rule(u16_rule<It>& port)
  {
    port = boost::spirit::qi::uint_parser<uint16_t, 10, 2, 5>();
    port.name("valid port range: (10, 65535)");
  }

  typedef boost::fusion::vector
    < std::uint16_t
    , boost::optional<std::uint16_t>
    > port_range_type
  ;

  template<class It>
  struct port_range_grammar
    : boost::spirit::qi::grammar
      < It
      , port_range_type()
      >
  {
    typedef typename port_range_grammar::base_type::sig_type signature;

    port_range_grammar()
      : port_range_grammar::base_type(start, "port_range")
    {
      init_port_rule(port);
      using namespace boost::spirit::qi;
      start = port > -(lit("-") > port);
    }

  private:
    boost::spirit::qi::rule<It, signature> start;
    boost::spirit::qi::rule<It, std::uint16_t()> port;
  };

我有些困惑如何定义一个范围,让 port1 的值小于 port2。我认为我需要在这里使用 eps 解析器,但似乎找不到正确的指定方式。欢迎提出任何建议。


相关问题 https://dev59.com/nFjUa4cB1Zd3GeqPR3wn 和 https://stackoverflow.com/a/30385909/85371 - sehe
2个回答

2

好的,我想我已经弄清楚了...


(注:保留html标签)
port_range_grammar()
  : port_range_grammar::base_type(start, "port_range")
{
  init_port_rule(port);
  using namespace boost::spirit::qi;
  namespace pnx = boost::phoenix;
  namespace fus = boost::fusion;
  start = port > -(lit("-") > port)
               > eps( pnx::bind
                       ( [](auto const& parsed)
                         {
                           if(!fus::at_c<1>(parsed).is_initialized())
                             return true;

                           auto lhs = fus::at_c<0>(parsed);
                           auto rhs = *fus::at_c<1>(parsed);
                           return lhs < rhs;
                         }
                       , _val
                       )
                    )
  ;
}

这个想法是将解析后的值传递给eps解析器,该解析器将检查构造的port_range_type是否具有第一个元素小于第二个元素。

哇,真是英雄所为。不过...也有点可怕 :) 很高兴地告诉你,我的答案列表中列出了大约6种不那么可怕的方法。 - sehe
不过别担心,你已经表现出了对语义动作、Phoenix演员、融合序列和语义谓词机制的理解。这种亲身实践经验是无价的,它永远无法被阅读或抄袭他人所学到的任何东西所取代。我给你点赞。 - sehe
@sehe:谢谢你的出色回答!这绝对非常有见地。我以前使用过语义动作,我的主要困难是理解将传递给 eps解析器的是什么。无论是 _val 还是 _1 甚至是 {_1,_2} :( 最后,如果您能解释一下为什么我的方法被视为可怕的(除了我使用 fusion::vector而不是手工制作的类型),我将不胜感激。 - ovanes
让我看看:需要绑定,需要at_c,需要使用魔法索引,需要在解析器表达式中硬编码逻辑,以一种使得很难理解正在测试什么的方式(事实上,我认为很少有人会猜到正在测试什么)。不过,非正式地说,你可以问路人他们更喜欢阅读哪个代码 :) - sehe
不要太在意,我很欣赏你提出这个想法,就像我之前说的。如果我认为这是一个坏主意,我是不会点赞这个回答的。事实上,我认为它可能有助于其他人理解它的工作原理,就像你发现它一样。感谢你的贡献! - sehe
1
感谢您的赞美和指出可以改进的地方 :) - ovanes

2

您确实可以使用语义动作。不过,您并不总是需要将它们附加到一个 eps 节点上。如果您这样做,您将得到以下结果:

port %= uint_parser<uint16_t, 10, 2, 5>() >> eps[ _pass = (_val>=10 && _val<=65535) ];
start = (port >> -('-' >> port)) >> eps(validate(_val));

请注意,其中一个规则使用带语义动作的“Simple Form eps”。这需要使用operator%=依然调用自动属性传播
第二个实例使用了“Semantic Predicate 形式的 eps”。validate函数需要是Phoenix Actor,我将其定义为:
struct validations {
    bool operator()(PortRange const& range) const {
        if (range.end)
            return range.start<*range.end;
        return true;
    }
};
boost::phoenix::function<validations> validate;

更为通用/一致性

请注意,您可以在两个规则上都使用第二种规则样式,如下所示:

port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
start = (port >> -('-' >> port))      >> eps(validate(_val));

如果您只是添加了一个重载方法来验证单个端口:

struct validations {
    bool operator()(Port const& port) const {
        return port>=10 && port<=65535;
    }
    bool operator()(PortRange const& range) const {
        if (range.end)
            return range.start<*range.end;
        return true;
    }
};

首次测试

让我们定义一些好的边缘情况并进行测试!

在Coliru上实时查看

#include <boost/fusion/adapted/struct.hpp>
#include <boost/optional/optional_io.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
namespace qi = boost::spirit::qi;

using Port = std::uint16_t;

struct PortRange {
    Port start;
    boost::optional<Port> end;
};

BOOST_FUSION_ADAPT_STRUCT(PortRange, start, end)

template <class It, typename Attr = PortRange> struct port_range_grammar : qi::grammar<It, Attr()> {

    port_range_grammar() : port_range_grammar::base_type(start, "port_range") {
        using namespace qi;

        port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
        start = (port >> -('-' >> port))      >> eps(validate(_val));

        port.name("valid port range: (10, 65535)");
    }

  private:
    struct validations {
        bool operator()(Port const& port) const {
            return port>=10 && port<=65535;
        }
        bool operator()(PortRange const& range) const {
            if (range.end)
                return range.start<*range.end;
            return true;
        }
    };
    boost::phoenix::function<validations> validate;
    qi::rule<It, Attr()> start;
    qi::rule<It, Port()> port;
};

int main() {
    using It = std::string::const_iterator;
    port_range_grammar<It> const g;

    std::string const valid[]   = {"10", "6322", "6322-6325", "65535"};
    std::string const invalid[] = {"9", "09", "065535", "65536", "-1", "6325-6322"};

    std::cout << " -------- valid cases\n";
    for (std::string const input : valid) {
        It f=input.begin(), l = input.end();
        PortRange range;
        bool accepted = parse(f, l, g, range);
        if (accepted)
            std::cout << "Parsed '" << input << "' to " << boost::fusion::as_vector(range) << "\n";
        else
            std::cout << "TEST FAILED '" << input << "'\n";
    }

    std::cout << " -------- invalid cases\n";
    for (std::string const input : invalid) {
        It f=input.begin(), l = input.end();
        PortRange range;
        bool accepted = parse(f, l, g, range);
        if (accepted)
            std::cout << "TEST FAILED '" << input << "' (returned " << boost::fusion::as_vector(range) << ")\n";
    }
}

输出:

 -------- valid cases
Parsed '10' to (10 --)
Parsed '6322' to (6322 --)
Parsed '6322-6325' to (6322  6325)
Parsed '65535' to (65535 --)
 -------- invalid cases
TEST FAILED '065535' (returned (6553 --))

恭喜!我们发现了一个破损的特殊情况。

原来限制uint_parser为5个位置,可能会导致输入中留下字符,因此065535解析为6553(留下未解析的'5'...)。修复方法很简单:

start = (port >> -('-' >> port)) >> eoi >> eps(validate(_val));

或者说:
start %= (port >> -('-' >> port)) >> eoi[ _pass = validate(_val) ];

修复版本在Coliru上实时运行

关于属性类型的几句话

你可能已经注意到我修改了你的属性类型。大部分是出于“好品味”。请注意,在实践中,您可能希望将范围表示为单端口或范围:

using Port = std::uint16_t;

struct PortRange {
    Port start, end;
};

using PortOrRange = boost::variant<Port, PortRange>;

然后您可以像这样解析:

port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
range = (port >> '-' >> port)         >> eps(validate(_val));

start = (range | port) >> eoi;

完整演示:在Coliru上实时演示

你可能认为这会变得难以使用。 我同意!

简化而非增加

让我们首先不使用variantoptional。 让我们将单个端口仅作为范围,该范围恰好具有start==end

using Port = std::uint16_t;

struct PortRange {
    Port start, end;
};

像这样解析:

start = port >> -('-' >> port | attr(0)) >> eoi >> eps(validate(_val));

validate中,我们所做的就是检查end是否为0

    bool operator()(PortRange& range) const {
        if (range.end == 0) 
            range.end = range.start;
        return range.start <= range.end;
    }

现在的输出结果是:在Coliru上实时运行

 -------- valid cases
Parsed '10' to (10-10)
Parsed '6322' to (6322-6322)
Parsed '6322-6325' to (6322-6325)
Parsed '65535' to (65535-65535)
 -------- invalid cases

请注意,您现在可以始终枚举start..end而无需知道是端口还是端口范围。这可能很方便(根据您实现的逻辑而有所不同)。

也许这个答案最重要的收获是:永远不要忘记正确性(我认为对“065535”的错误解析比任何样式问题都更糟糕。虽然我认为它们是相关的。使事情易于处理可以更轻松地编写测试;花费更多时间思考边缘情况,减少修复它们的努力。) - sehe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接