为了帮助创建健壮的正则表达式和语法,以下是一些关于代码布局和可读性、实际匹配内容以及避免常见陷阱的最佳实践。

代码布局§

在没有 :sigspace 副词的情况下,空白在 Raku 正则表达式中并不重要。利用这一点,在需要提高可读性的位置插入空白。此外,在必要时插入注释。

比较非常紧凑的

my regex float { <[+-]>?\d*'.'\d+[e<[+-]>?\d+]? }

与更易读的

my regex float {
     <[+-]>?        # optional sign 
     \d*            # leading digits, optional 
     '.'
     \d+
     [              # optional exponent 
        e <[+-]>?  \d+
     ]?
}

作为经验法则,在原子周围和组内使用空白;将量词直接放在原子之后;并将打开和关闭方括号和圆括号垂直对齐。

当你在括号或方括号内使用一系列替换时,请对齐竖线

my regex example {
    <preamble>
    [
    || <choice_1>
    || <choice_2>
    || <choice_3>
    ]+
    <postamble>
}

保持简洁§

正则表达式通常比普通代码更简洁。由于它们用很少的代码就能完成很多工作,因此要保持正则表达式的简短。

当你可以命名正则表达式的一部分时,通常最好将其放入一个单独的命名正则表达式中。

例如,你可以从前面的示例中获取浮点数正则表达式

my regex float {
     <[+-]>?        # optional sign 
     \d*            # leading digits, optional 
     '.'
     \d+
     [              # optional exponent 
        e <[+-]>?  \d+
     ]?
}

并将其分解成各个部分

my token sign { <[+-]> }
my token decimal { \d+ }
my token exponent { 'e' <sign>? <decimal> }
my regex float {
    <sign>?
    <decimal>?
    '.'
    <decimal>
    <exponent>?
}

这很有帮助,尤其是在正则表达式变得更加复杂时。例如,你可能希望在存在指数的情况下使小数点可选。

my regex float {
    <sign>?
    [
    || <decimal>?  '.' <decimal> <exponent>?
    || <decimal> <exponent>
    ]
}

匹配内容§

通常,输入数据格式没有明确的规范,或者程序员不知道规范。在这种情况下,最好对期望的内容持宽松态度,但前提是不能存在任何可能的歧义。

例如,在 ini 文件中

[section]
key=value

节标题内部可以包含什么内容?只允许一个单词可能过于严格。有人可能会写 [two words],或者使用连字符等。与其询问内部允许什么,不如反过来问:不允许什么?

显然,不允许出现闭合方括号,因为 [a]b] 会产生歧义。根据相同的论据,应该禁止出现开头的方括号。这让我们得到了

token header { '[' <-[ \[\] ]>+ ']' }

如果只处理一行,这就可以了。但是,如果你要处理整个文件,正则表达式突然解析

[with a
newline in between]

这可能不是一个好主意。一个折衷方案是

token header { '[' <-[ \[\] \n ]>+ ']' }

然后,在后处理中,从节标题中删除前导和尾随空格和制表符。

匹配空白§

:sigspace 副词(或使用 rule 声明符而不是 tokenregex)对于隐式解析可能出现在许多地方的空白非常有用。

回到解析 ini 文件的示例,我们有

my regex kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }

这可能不像我们希望的那样宽松,因为用户可能会在等号周围添加空格。因此,我们可以尝试以下方法

my regex kvpair { \s* <key=identifier> \s* '=' \s* <value=identifier> \n+ }

但这看起来很笨拙,所以我们尝试其他方法

my rule kvpair { <key=identifier> '=' <value=identifier> \n+ }

但是等等!值后面的隐式空白匹配会消耗掉所有空白,包括换行符,因此 \n+ 没有剩余的内容可以匹配(并且 rule 也禁用了回溯,所以在这里没有用)。

因此,重要的是重新定义隐式空白的定义,使其成为输入格式中不重要的空白。

这通过重新定义令牌 ws 来实现;但是,它只适用于 语法

grammar IniFormat {
    token ws { <!ww> \h* }
    rule header { \s* '[' (\w+']' \n+ }
    token identifier  { \w+ }
    rule kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
    token section {
        <header>
        <kvpair>*
    }
 
    token TOP {
        <section>*
    }
}
 
my $contents = q:to/EOI/; 
    [passwords]
        jack = password1
        joy = muchmoresecure123
    [quotas]
        jack = 123
        joy = 42
EOI
say so IniFormat.parse($contents);

除了将所有正则表达式放入语法中并将其转换为令牌(因为它们不需要回溯)之外,有趣的新部分是

token ws { <!ww> \h* }

它在隐式空白解析时被调用。当它不在两个单词字符之间(<!ww>,否定“在单词内”断言)时,它会匹配零个或多个水平空格字符。限制为水平空白很重要,因为换行符(垂直空白)分隔记录,不应该被隐式匹配。

尽管如此,仍然存在一些与空白相关的麻烦。正则表达式 \n+ 不会匹配像 "\n \n" 这样的字符串,因为两个换行符之间有一个空格。为了允许这样的输入字符串,请将 \n+ 替换为 \n\s*