【问题标题】:Regex for matching C++ string constant用于匹配 C++ 字符串常量的正则表达式
【发布时间】:2017-06-14 00:50:55
【问题描述】:

我目前正在使用 C++ 预处理器,我需要匹配超过 0 个字母的字符串常量,例如 "hey I'm a string。 我目前正在使用这个\"([^\\\"]+|\\.)+\",但它在我的一个测试用例中失败了。

测试用例:

std::cout << "hello" << " world";
std::cout << "He said: \"bananas\"" << "...";
std::cout << "";
std::cout << "\x12\23\x34";

预期输出:

std::cout << String("hello") << String(" world");
std::cout << String("He said: \"bananas\"") << String("...");
std::cout << "";
std::cout << String("\x12\23\x34");

第二个我反而得到了

std::cout << String("He said: \")bananas\"String(" << ")...";

简短的复制代码(使用 AR.3 的正则表达式):

std::string in_line = "std::cout << \"He said: \\\"bananas\\\"\" << \"...\";";
std::regex r("\"([^\"]+|\\.|(?<=\\\\)\")+\"");
in_line = std::regex_replace(in_line, r, "String($&)");

【问题讨论】:

  • 别忘了正确处理原始字符串 ;)
  • @LucasTrzesniewski 有区别吗?都有 \" 和 \\...
  • @NiclasM u8R"hello(this"is\a\""""single\\valid raw string literal)hello"
  • 用正则表达式解析 C++ 源代码是徒劳的。你至少需要一个有状态的词法分析器。
  • 也在/* "comments" */(可以嵌套)、多行和原始字符串中,跳过'"'等内容?

标签: c++ regex string c-preprocessor


【解决方案1】:

对源文件进行词法分析正则表达式的好工作。但是对于这样的任务,让我们使用比std::regex 更好的正则表达式引擎。我们首先使用 PCRE(或boost::regex)。在这篇文章的最后,我将展示使用功能较少的引擎可以做什么。

我们只需要进行部分词法分析,忽略所有不会影响字符串文字的无法识别的标记。我们需要处理的是:

  • 单行 cmets
  • 多行 cmets
  • 字符字面量
  • 字符串字面量

我们将使用扩展的 (x) 选项,它会忽略模式中的空格。

评论

[lex.comment] 是这样说的:

字符/* 开始注释,注释以字符*/ 结束。这些 cmets 不筑巢。 字符// 开始一个注释,它在下一个换行符之前立即终止。如果 此类评论中有换页符或垂直制表符,只能出现空白字符 在它和终止注释的换行符之间;不需要诊断。 [注:评论 字符 ///**/// 注释中没有特殊含义,并且与其他字符一样对待 人物。同样,注释字符///*/* 注释中没有特殊含义。 ——结束注]

# singleline comment
// .* (*SKIP)(*FAIL)

# multiline comment
| /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)

简单易懂。如果你在那里匹配任何东西,只需(*SKIP)(*FAIL) - 这意味着你扔掉了匹配。 (?s: .*? )s(单行)修饰符应用于. 元字符,这意味着它可以匹配换行符。

字符字面量

这是来自[lex.ccon]的语法:

 character-literal:  
    encoding-prefix(opt) ’ c-char-sequence ’
  encoding-prefix:
    one of u8 u U L
  c-char-sequence:
    c-char
    c-char-sequence c-char
  c-char:
    any member of the source character set except the single-quote ’, backslash \, or new-line character
    escape-sequence
    universal-character-name
  escape-sequence:
    simple-escape-sequence
    octal-escape-sequence
    hexadecimal-escape-sequence
  simple-escape-sequence: one of \’ \" \? \\ \a \b \f \n \r \t \v
  octal-escape-sequence:
    \ octal-digit
    \ octal-digit octal-digit
    \ octal-digit octal-digit octal-digit
  hexadecimal-escape-sequence:
    \x hexadecimal-digit
    hexadecimal-escape-sequence hexadecimal-digit

让我们先定义一些东西,我们稍后会用到:

(?(DEFINE)
  (?<prefix> (?:u8?|U|L)? )
  (?<escape> \\ (?:
    ['"?\\abfnrtv]         # simple escape
    | [0-7]{1,3}           # octal escape
    | x [0-9a-fA-F]{1,2}   # hex escape
    | u [0-9a-fA-F]{4}     # universal character name
    | U [0-9a-fA-F]{8}     # universal character name
  ))
)
  • prefix 定义为可选的u8uUL
  • escape 是按照标准定义的,只是为了简单起见,我将 universal-character-name 合并到其中

一旦我们有了这些,字符文字就非常简单了:

(?&prefix) ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)

我们用(*SKIP)(*FAIL)把它扔掉

简单字符串

它们的定义方式几乎与字符文字相同。这是[lex.string]的一部分:

  string-literal:
    encoding-prefix(opt) " s-char-sequence(opt) "
    encoding-prefix(opt) R raw-string
  s-char-sequence:
    s-char
    s-char-sequence s-char
  s-char:
    any member of the source character set except the double-quote ", backslash \, or new-line character
    escape-sequence
    universal-character-name

这将反映字符文字:

(?&prefix) " (?> (?&escape) | [^"\\\r\n]+ )* "

区别在于:

  • 这次字符序列是可选的(* 而不是+
  • 未转义时不允许使用双引号而不是单引号
  • 我们实际上不会把它扔掉:)

原始字符串

这是原始字符串部分:

  raw-string:
    " d-char-sequence(opt) ( r-char-sequence(opt) ) d-char-sequence(opt) "
  r-char-sequence:
    r-char
    r-char-sequence r-char
  r-char:
    any member of the source character set, except a right parenthesis )
    followed by the initial d-char-sequence (which may be empty) followed by a double quote ".
  d-char-sequence:
    d-char
    d-char-sequence d-char
  d-char:
    any member of the basic source character set except:
    space, the left parenthesis (, the right parenthesis ), the backslash \,
    and the control characters representing horizontal tab,
    vertical tab, form feed, and newline.

这个的正则表达式是:

(?&prefix) R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "
  • [^ ()\\\t\x0B\r\n]* 是分隔符中允许的字符集 (d-char)
  • \k&lt;delimiter&gt; 指的是之前匹配的分隔符

完整模式

完整的模式是:

(?(DEFINE)
  (?<prefix> (?:u8?|U|L)? )
  (?<escape> \\ (?:
    ['"?\\abfnrtv]         # simple escape
    | [0-7]{1,3}           # octal escape
    | x [0-9a-fA-F]{1,2}   # hex escape
    | u [0-9a-fA-F]{4}     # universal character name
    | U [0-9a-fA-F]{8}     # universal character name
  ))
)

# singleline comment
// .* (*SKIP)(*FAIL)

# multiline comment
| /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)

# character literal
| (?&prefix) ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)

# standard string
| (?&prefix) " (?> (?&escape) | [^"\\\r\n]+ )* "

# raw string
| (?&prefix) R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "

the demo here

boost::regex

这是一个使用boost::regex的简单演示程序:

#include <string>
#include <iostream>
#include <boost/regex.hpp>

static void test()
{
    boost::regex re(R"regex(
        (?(DEFINE)
          (?<prefix> (?:u8?|U|L) )
          (?<escape> \\ (?:
            ['"?\\abfnrtv]         # simple escape
            | [0-7]{1,3}           # octal escape
            | x [0-9a-fA-F]{1,2}   # hex escape
            | u [0-9a-fA-F]{4}     # universal character name
            | U [0-9a-fA-F]{8}     # universal character name
          ))
        )

        # singleline comment
        // .* (*SKIP)(*FAIL)

        # multiline comment
        | /\* (?s: .*? ) \*/ (*SKIP)(*FAIL)

        # character literal
        | (?&prefix)? ' (?> (?&escape) | [^'\\\r\n]+ )+ ' (*SKIP)(*FAIL)

        # standard string
        | (?&prefix)? " (?> (?&escape) | [^"\\\r\n]+ )* "

        # raw string
        | (?&prefix)? R " (?<delimiter>[^ ()\\\t\x0B\r\n]*) \( (?s:.*?) \) \k<delimiter> "
    )regex", boost::regex::perl | boost::regex::no_mod_s | boost::regex::mod_x | boost::regex::optimize);

    std::string subject(R"subject(
std::cout << L"hello" << " world";
std::cout << "He said: \"bananas\"" << "...";
std::cout << "";
std::cout << "\x12\23\x34";
std::cout << u8R"hello(this"is\a\""""single\\(valid)"
raw string literal)hello";

"" // empty string
'"' // character literal

// this is "a string literal" in a comment
/* this is
   "also inside"
   //a comment */

// and this /*
"is not in a comment"
// */

"this is a /* string */ with nested // comments"
    )subject");

    std::cout << boost::regex_replace(subject, re, "String\\($&\\)", boost::format_all) << std::endl;
}

int main(int argc, char **argv)
{
    try
    {
        test();
    }
    catch(std::exception ex)
    {
        std::cerr << ex.what() << std::endl;
    }

    return 0;
}

(我禁用了语法高亮,因为这段代码太疯狂了)

出于某种原因,我不得不将? 量词从prefix 中取出(将(?&lt;prefix&gt; (?:u8?|U|L)? ) 更改为(?&lt;prefix&gt; (?:u8?|U|L) ) 并将(?&amp;prefix) 更改为(?&amp;prefix)?)以使模式起作用。我相信这是 boost::regex 中的一个错误,因为 PCRE 和 Perl 在原始模式下都可以正常工作。

如果我们手头没有花哨的正则表达式引擎怎么办?

请注意,虽然此模式在技术上使用递归,但它从不嵌套递归调用。可以通过将相关的可重用部分内联到主模式中来避免递归。

可以以降低性能为代价来避免其他一些构造。如果我们不嵌套量词以避免catastrophic backtracking,我们可以安全地将原子组(?&gt;...) 替换为普通组(?:...)

如果我们在替换函数中添加一行逻辑,我们也可以避免(*SKIP)(*FAIL):所有要跳过的替代项都分组在一个捕获组中。如果捕获组匹配,则忽略匹配。如果不是,那么它是一个字符串文字。

所有这一切意味着我们可以在 JavaScript 中实现这一点,它具有您能找到的最简单的正则表达式引擎之一,但代价是打破 DRY 规则并使模式难以辨认。正则表达式一旦转换就变成了这个怪物:

(\/\/.*|\/\*[\s\S]*?\*\/|(?:u8?|U|L)?'(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^'\\\r\n])+')|(?:u8?|U|L)?"(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^"\\\r\n])*"|(?:u8?|U|L)?R"([^ ()\\\t\x0B\r\n]*)\([\s\S]*?\)\2"

这是一个您可以玩的交互式演示:

function run() {
    var re = /(\/\/.*|\/\*[\s\S]*?\*\/|(?:u8?|U|L)?'(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^'\\\r\n])+')|(?:u8?|U|L)?"(?:\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})|[^"\\\r\n])*"|(?:u8?|U|L)?R"([^ ()\\\t\x0B\r\n]*)\([\s\S]*?\)\2"/g;
    
    var input = document.getElementById("input").value;
    var output = input.replace(re, function(m, ignore) {
        return ignore ? m : "String(" + m + ")";
    });
    document.getElementById("output").innerText = output;
}

document.getElementById("input").addEventListener("input", run);
run();
<h2>Input:</h2>
<textarea id="input" style="width: 100%; height: 50px;">
std::cout << L"hello" << " world";
std::cout << "He said: \"bananas\"" << "...";
std::cout << "";
std::cout << "\x12\23\x34";
std::cout << u8R"hello(this"is\a\""""single\\(valid)"
raw string literal)hello";

"" // empty string
'"' // character literal

// this is "a string literal" in a comment
/* this is
   "also inside"
   //a comment */

// and this /*
"is not in a comment"
// */

"this is a /* string */ with nested // comments"
</textarea>
<h2>Output:</h2>
<pre id="output"></pre>

【讨论】:

  • 我打开了一个提升错误报告here
【解决方案2】:

正则表达式对于初学者来说可能很棘手,但一旦您了解它的基础知识并经过充分测试的分而治之策略,它将成为您的 goto 工具。

您需要搜索不以 () 反斜杠开头的引号 (") 并读取下一个引号之前的所有字符。

我想出的正则表达式是(".*?[^\\]")。请参阅下面的代码 sn-p。

std::string in_line = "std::cout << \"He said: \\\"bananas\\\"\" << \"...\";";

std::regex re(R"((".*?[^\\]"))");
in_line = std::regex_replace(in_line, re, "String($1)");

std::cout << in_line << endl;

输出:

std::cout << String("He said: \"bananas\"") << String("...");

正则表达式解释:

(".*?[^\\]")

选项:区分大小写;编号捕获;允许零长度匹配;仅限正则表达式语法

  • 匹配下面的正则表达式并将其匹配捕获到反向引用号 1 (".*?[^\\]")
    • 匹配字符“"”字面意思"
    • 匹配任何不是换行符的单个字符(换行符、回车符).*?
      • 在零次和无限次之间,尽可能少,根据需要扩展(懒惰)*?
    • 匹配任何非反斜杠字符[^\\]
    • 匹配字符“"”字面意思"

字符串($1)

  • 按字面意思插入字符串“String”String
  • 插入左括号(
  • 插入与捕获组号 1 上次匹配的文本 $1
  • 插入右括号)

【讨论】:

  • 此解决方案与空字符串 ("") 不匹配。小的改动也能找到: (".*?[^\\]?") 解释: [^\\] 要求至少有一个不是反斜杠的字符。添加 ?在这部分使其成为可选之后。
【解决方案3】:

阅读 C++ 标准中的相关部分,它们被称为 lex.cconlex.string

然后将您在其中找到的每个规则转换为正则表达式(如果您真的想使用正则表达式;可能会证明它们无法完成这项工作)。

然后,用它们构建更复杂的正则表达式。请务必将您的正则表达式命名为与 C++ 标准中的规则完全一致,以便您以后可以重新检查它们。

如果您想使用现有工具而不是使用正则表达式,请使用以下工具:http://clang.llvm.org/doxygen/Lexer_8cpp_source.html。看看LexStringLiteral 函数。

【讨论】:

  • 我不了解你,但我没有时间。正则表达式的替代品是什么?
  • 使用解析 C++ 字符串文字的现有库。它可能在编译器的源代码中,或者在源代码格式化程序中,或者在具有语法高亮显示的 IDE 中。所有这些程序都已经解决了这个问题。在查看它们时,请从您最信任的那些正确处理所有边缘情况的人开始。
  • 你刚刚学到了关于正则表达式最重要的一点:它们不能解决所有的任务。哦,不,等等。你还没学会。要真正学习它,请尝试解析 C++ 字符串文字。即使你失败了,之后你也会对正则表达式了解更多。
  • @RolandIllig 肯定正则表达式不能解决所有任务,但我厌倦了人们说正则表达式不适合他们设计的任务。 C++ 字面量是 Chomsky Type-3 语言,它们是常规的,正则表达式非常适合。我试图在我的回答中遵循标准只是为了表明它根本没有问题。
  • @LucasTrzesniewski C++ 原始字符串文字是不是 Chomsky Type 3,因为反向引用\2。因此传统的正则表达式无法匹配它们。增强的正则表达式(如在 Perl、JavaScript 中)可以 匹配它们,但这些不再是 regular
猜你喜欢
  • 2011-06-24
  • 1970-01-01
  • 2011-12-21
  • 1970-01-01
  • 2019-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-16
相关资源
最近更新 更多