用于匹配C++字符串常量的正则表达式

7

我目前正在开发一个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($&)");

2
不要忘记正确处理原始字符串 ;) - Lucas Trzesniewski
5
@NiclasM u8R"hello(this"is\a\""""single\\valid raw string literal)hello" - Lucas Trzesniewski
4
使用正则表达式解析C++源代码是徒劳无功的。至少需要一个具有状态的词法分析器。 - rustyx
3
同样在 /* "comments" */ 内(可以嵌套),包括多行和原始字符串,跳过像 '"' 等的内容。 - rustyx
是的,我知道预处理器是什么,包括特别指C++预处理器。通常人们不会构建一个没有明确想法的C++预处理器。 - Ira Baxter
显示剩余16条评论
3个回答

5
Lexing一个源代码文件使用regex是一个不错的选择。但是对于这样的任务,我们应该使用比std::regex更好的regex引擎。让我们先使用PCRE(或boost::regex)。在本文末尾,我将展示如何使用功能更少的引擎。
我们只需要做部分词法分析,忽略所有不影响字符串字面量的未识别标记。我们需要处理以下内容:
单行注释 多行注释 字符字面量 字符串字面量
我们将使用扩展(x)选项,在模式中忽略空格。
评论
下面是lex.comment的规定:
字符 /* 开始了一个注释,以字符 */ 结束。这些注释不嵌套。 字符 // 开始一个注释,在接下来的新行字符之前立即终止。如果在这样的注释中有一个换页符或垂直制表符字符,则仅在它和终止注释的新行之间出现空白字符;不需要进行诊断。[注意:注释字符 //,/* 和*/ 在//注释中没有特殊意义,与其他字符一样被处理。同样,注释字符//和/*在/*注释中没有特殊意义。-注意]
# 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被定义为可选的u8, u, UL
  • 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<delimiter>指代先前匹配到的限定符

完整的模式

完整的模式如下:

(?(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> "

请查看此示例

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 中删除 ? 量词(将 (?<prefix> (?:u8?|U|L)? ) 更改为 (?<prefix> (?:u8?|U|L) ) 并将 (?&prefix) 更改为 (?&prefix)?)才能使该模式正常工作。我认为这是 boost::regex 的一个 bug,因为 PCRE 和 Perl 都可以很好地处理原始模式。

如果我们没有高级的正则表达式引擎怎么办?

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

为了避免灾难性回溯,如果我们不嵌套量词,就可以安全地将原子组(?>...)替换为普通组(?: ...)

如果我们将所有跳过的替代方案分组到一个捕获组中,则可以避免使用(*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>


我已经在这里打开了一个 Boost 的 bug 报告 链接 - Lucas Trzesniewski

3

正则表达式对于初学者来说可能很棘手,但是一旦掌握了其基础知识和经过良好测试的分治策略,它将成为您的首选工具。

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

我想出的正则表达式是(".*?[^\\]")。请参见下面的代码片段。

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中(".*?[^\\]")
    • 匹配字符“"”"
    • 匹配任何不是换行符(换行,回车)的单个字符.*?
      • 最少匹配0次,尽可能少地进行匹配(惰性模式)*?
    • 匹配不是反斜杠字符的任何字符[^\\]
    • 匹配字符“"”"

字符串($1)

  • 插入字符字符串“String”String
  • 插入左括号(
  • 插入由捕获组编号1捕获的文本$1
  • 插入右括号)

这个解决方案不适用于空字符串(“”)。 小的修改也可以找到这些:(“.*?[^\]?”) 解释:[^\] 要求至少有一个不是反斜杠的字符。在此部分后面添加 ? 使其变为可选项。 - Bojan Hrnkas

2

请阅读C++标准相关的章节,它们被称为lex.cconlex.string

然后将您找到的每个规则转换成正则表达式(如果您确实想使用正则表达式;可能会发现它们无法完成此工作)。

然后,从中构建更复杂的正则表达式。一定要按照C++标准的规则名称精确地命名您的正则表达式,以便稍后重新检查它们。

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


我不知道你怎么想,但我没时间搞这个。正则表达式的替代方案有哪些? - Niclas M
使用现有的库来解析C++字符串字面量。它可以在编译器的源代码、源代码格式化程序或具有语法高亮的IDE中。所有这些程序都已经解决了这个问题。当查看它们时,首先从那些你最信任的处理所有边缘情况的程序开始。 - Roland Illig
这是我最初想到的,但后来我想更深入地学习正则表达式。 - Niclas M
1
你刚刚学到了关于正则表达式最重要的一点:它们不能解决所有任务。哦,不,等等。你还没有学到这一点。为了真正学会它,尝试解析C++字符串字面值。即使你失败了,之后你也会对正则表达式有更多的了解。 - Roland Illig
1
@RolandIllig 确实,正则表达式不能解决所有任务,但我已经厌倦了人们说即使是为了设计而创建的任务,正则表达式也不适合的说法。C++文字字面量是一种 Chomsky Type-3 语言,它们是规则的,正则表达式对此非常适用。我尝试在我的答案中遵循标准,只是为了证明这根本不是问题。 - Lucas Trzesniewski
2
@LucasTrzesniewski C++原始字符串字面量由于反向引用\2而不是Chomsky Type 3。因此,传统的正则表达式无法匹配它们。增强的正则表达式(如Perl、JavaScript中的)可以匹配它们,但这些不再是_正则_的了。 - Roland Illig

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