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
, U
或L
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>
u8R"hello(this"is\a\""""single\\valid raw string literal)hello"
- Lucas Trzesniewski/* "comments" */
内(可以嵌套),包括多行和原始字符串,跳过像'"'
等的内容。 - rustyx