【问题标题】:Why does this regex run differently in sed than in Perl/Ruby?为什么这个正则表达式在 sed 中的运行方式与在 Perl/Ruby 中的运行方式不同?
【发布时间】:2014-01-11 17:47:39
【问题描述】:

我有一个正则表达式,它在sed 中给出了一个结果,但在 Perl(和 Ruby)中给出了另一个结果。

我有字符串one;two;;three,我想突出显示由; 分隔的子字符串。所以我在 Perl 中执行以下操作:

$a = "one;two;;three";
$a =~ s/([^;]*)/[\1]/g;
print $a;

(或者,在 Ruby 中:print "one;two;;three".gsub(/([^;]*)/, "[\\1]")。)

结果是:

[one][];[two][];[];[three][]

(我知道虚假空子字符串的原因。)

奇怪的是,当我在 sed 中运行相同的正则表达式时,我得到了不同的结果。我跑:

echo "one;two;;three" | sed -e 's/[^;]*/[\0]/g'

我得到:

[one];[two];[];[three]

造成这种不同结果的原因是什么?

编辑:

有人回答“因为sed 不是perl”。我知道。我问这个问题的原因是因为我不明白 sed 如何很好地应对零长度匹配。

【问题讨论】:

  • sed 不能“应付 ... 零长度匹配” - 它以一种恰好满足您需要的方式出错。改写join ';', map "[$_]", split /;/, $str,不要使用a作为标识符。
  • 由于我对 perl 和 ruby​​ 缺乏了解,我无法回答这个问题。然而,这是一个很好的问题。只是被问到的方式不对... +1 无论如何...等待了解 perl/ruby 如何处理 greedy 匹配。
  • @NiccoloM。你是说读 Perl 标签的人思想不开放?这是多么不公平和不合理的话。
  • @TLP:我把它理解为“通常使用 perl,但看到带有[perl] 标记的内容的人可能仅根据标签。”
  • @mirabilos: $a$b 被 perl 用于排序:perldoc.perl.org/functions/sort.html 避免在其他上下文中使用它们只是一种好的 perl 做法。

标签: ruby regex perl sed


【解决方案1】:

这是一个有趣且令人惊讶的边缘案例。

你的[^;]* 模式可能匹配空字符串,所以它变成了一个哲学问题,,两个字符之间有多少个空字符串:零,一个,还是很多?

sed

sed 匹配显然遵循“Zero–Length Regex Matches.” 的“在零长度正则表达式匹配后推进”部分中描述的理念

现在正则表达式引擎处于一个棘手的情况。我们要求它遍历整个字符串以查找所有不重叠的正则表达式匹配。第一个匹配在字符串的开头结束,第一次匹配尝试开始的地方。正则表达式引擎需要一种方法来避免陷入无限循环,该循环永远在字符串的开头找到相同的零长度匹配。

大多数正则表达式引擎使用的最简单的解决方案是,如果前一个匹配的长度为零,则在前一个匹配结束后尝试一个字符开始下一个匹配。

也就是说,字符之间有零个空字符串。

以上段落不是权威标准,引用这样的文件会更好地回答。

检查source of GNU sed,我们看到

/* Start after the match.  last_end is the real end of the matched
   substring, excluding characters that were skipped in case the RE
   matched the empty string.  */
start = offset + matched;
last_end = regs.end[0];

Perl 和 Ruby

​​>

Perl 与 s/// 的哲学,Ruby 似乎共享这一点——因此下面的文档和示例使用 Perl 来表示两者——每个字符之后正好有一个空字符串。

“Regexp Quote–Like Operators” section of the perlop 文档显示

/g 修饰符指定全局模式匹配,即在字符串中匹配尽可能多的次数。

跟踪s/([^;]*)/[\1]/g 的执行给了

  1. 开始。由^ 表示的“匹配位置”位于目标字符串的开头。

     o n e ; t w o ; ; t h r e e
    ^
    
  2. 尝试匹配[^;]*

     o n e ; t w o ; ; t h r e e
          ^
    

    注意$1 中捕获的结果是one

  3. 尝试匹配[^;]*

     o n e ; t w o ; ; t h r e e
          ^
    

    重要教训:* 正则表达式量词总是成功,因为它意味着“零或更多”。在这种情况下,$1 中的子字符串是空字符串。

比赛的其余部分按上述进行。

作为一个敏锐的读者,你现在问自己,“自己,如果* 总是成功,匹配如何在目标字符串的末尾终止,或者就此而言,它是如何超过第一个零的——长度匹配?”

我们在“Repeated Patterns Matching a Zero–length Substring” section of the perlre 文档中找到了这个尖锐问题的答案。

然而,长期经验表明,通过使用可能匹配零长度子字符串的重复子表达式,可以显着简化许多编程任务。这是一个简单的例子:

@chars = split //, $string; # // is not magic in split
($whitewashed = $string) =~ s/()/ /g; # parens avoid magic s// /

因此,Perl 通过强行打破无限循环来允许这样的构造。对于由贪心量词*+{} 给出的低级循环和/g 修饰符或split 运算符等高级循环,此规则是不同的。

更高级别的循环在迭代之间保留了一个附加状态:最后一次匹配是否为零长度。为了打破循环,零长度匹配之后的下一个匹配被禁止长度为零。此禁止与回溯相互作用……因此,如果最佳匹配的长度为零,则选择第二个最佳匹配。

其他 Perl 方法

通过添加否定的后向断言,您可以过滤虚假的空匹配。

  $ perl -le '$a = "one;two;;three";
              $a =~ s/(?<![^;])([^;]*)/[\1]/g;
              print $a;'
  [one];[two];[];[three]

应用 Mark Dominus 所说的 Randal’s Rule,“当您知道要保留什么时使用捕获。当您知道要丢弃什么时,请使用split。”你想扔掉分号,这样你的代码就变得更直接了

$ perl -le '$a = "one;two;;three";
            $a = join ";", map "[$_]", split /;/, $a;
            print $a;'
[one];[two];[];[three]

【讨论】:

  • 我刚刚编辑了您的答案以包含有关高级循环(/gsplit)而不是低级循环(*+{})的部分,但现在我不确定如果这是合适的,因为这两种类型都在这里发挥作用。随意回滚。
  • @ThisSuitIsBlackNot Yours 是更好的选择。感谢您改进答案。查看更新。
  • 感谢您的调查工作!您链接到的文章非常宝贵:它帮助我理解了事物。但我必须将最佳答案授予@tihom,因为他指出了原因。 (顺便说一句,阅读那篇文章我发现 Ruby 与 Perl 不同;他们讨论了在 "x1" 上匹配 /\d*|x/。Ruby 使用“[T]he 最简单的解决方案”,而 Perl 使用“[T]he other solution”。)
  • 第 3 项 Attempt to match [^;]*. 毫无意义。你不能有一个匹配“除 ; 之外的所有东西”的 RE然后在“;之前的所有内容”之间添加一些内容和 ”;”。 ; 之前的任何感知空字符串都是匹配everything before the ; 的字符串的一部分。 RE [^;]* 匹配“零个或多个不是 ;` 的字符,这并不意味着 zero or more character that are neither ; nor the null string
  • @EdMorton 也许这没有意义,但这正是 Perl 所做的。运行perl -Mre=debug -e '$_ = "one;two;;three"; s/([^;]*)/[$1]/g' 亲自查看。尽管情况略有不同,但 ikegami 给出了一个很好的解释,说明了它如何与零长度环视 here.
【解决方案2】:

来自sed-4.2 的替代函数的源代码:

   /sed/execute.c
  /* If we're counting up to the Nth match, are we there yet?
     And even if we are there, there is another case we have to
 skip: are we matching an empty string immediately following
     another match?

     This latter case avoids that baaaac, when passed through
     s,a*,x,g, gives `xbxxcx' instead of xbxcx.  This behavior is
     unacceptable because it is not consistently applied (for
     example, `baaaa' gives `xbx', not `xbxx'). */

这表明我们在 Ruby 和 Perl 中看到的行为在 sed 中是有意识地避免的。这不是由于语言之间的任何根本差异,而是sed 中特殊处理的结果

【讨论】:

  • 谢谢。这是一个准确的答案。文章Zero-Length Regex Matches 解释了处理零长度匹配的三种方法。第三种方法是“在前一个匹配结束的位置跳过零长度匹配,因此你永远不会有一个紧邻非零长度匹配的零长度匹配”,你刚刚证明了这一点sed 会。
  • sed 并不孤单。我测试了 python 甚至 vim 的正则表达式。都给出了与 sed 相同的结果。
  • re in Python 3.7 现在可以像 Perl 一样处理零长度匹配:在 3.7 版中更改:模式的空匹配在与之前的非空匹配相邻时被替换。
【解决方案3】:

在 perl(可能是 ruby​​)脚本中还有其他事情发生,因为该输出对于简单地将正则表达式作为 BRE 或 ERE 处理是没有意义的。

awk (EREs) 和 sed (BREs) 的行为与它们应有的行为一样,仅用于替换 RE:

$ echo "one;two;;three" | sed -e 's/[^;]*/[&]/g'
[one];[two];[];[three]

$ echo "one;two;;three" | awk 'gsub(/[^;]*/,"[&]")'
[one];[two];[];[three]

你说I know the reason for the spurious empty substrings.。愿意为我们提供线索吗?

【讨论】:

  • 至于“伪空子串的原因”:运行echo aaaa | sed -e 's/q*/[&amp;]/'看一个简单的案例。这些是“零长度匹配”(参见@Greg Bacon 回答的第一个链接)。正则表达式有两个基本规则:(1)尽可能早地匹配,(2)匹配最长的运行(除非我们不贪心)。 /q*/(上)匹配,因为它是最早的匹配。
  • 我理解零长度匹配。我在发布的文本和相关的 RE 中没有看到任何内容。 one 匹配 [^;]*。在它之后没有额外的零长度匹配。如果有,那么在每个匹配的 RE 之前和之后总会有无限数量的零长度匹配。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-30
相关资源
最近更新 更多