与 SED 正则表达式的非贪婪匹配(模拟 perl 的 .*?)

Bab*_*aba 37 sed text-processing regular-expression

我想用sed在第一之间的字符串替换任何AB和第一的发生AC(含)用XXX。

对于例如,我有这样的字符串(该字符串是仅用于测试):

ssABteAstACABnnACss
Run Code Online (Sandbox Code Playgroud)

我想类似这样的输出:ssXXXABnnACss。


我这样做了perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss
Run Code Online (Sandbox Code Playgroud)

但我想用sed. 以下(使用 Perl 兼容的正则表达式)不起作用:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
Run Code Online (Sandbox Code Playgroud)

Joh*_*024 28

Sed 正则表达式匹配最长的匹配。Sed 没有非贪婪的等价物。

我们要做的是匹配

  1. AB,
    其次是
  2. 除了AC,
    之后的任何数量
  3. AC

不幸的是,sed不能做 #2 — 至少不能用于多字符正则表达式。当然,对于单字符的正则表达式比如@(甚至[123]),我们可以做[^@]*or [^123]*。因此,我们可以通过更改所有出现的ACto@然后搜索来解决 sed 的限制

  1. AB,
    其次是
  2. 除了@,
    之后的任何数量
  3. @

像这样:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'
Run Code Online (Sandbox Code Playgroud)

最后一部分将不匹配的实例更改@回AC.

但这是一种鲁莽的方法,因为输入可能已经包含@字符。因此,通过匹配它们,我们可能会得到误报。但是,由于任何 shell 变量中都不会包含 NUL ( \x00) 字符,因此 NUL 可能是在上述解决方法中使用的好字符,而不是@:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss
Run Code Online (Sandbox Code Playgroud)

使用 NUL 需要 GNU sed。(要确保启用 GNU 功能,用户不得设置 shell 变量 POSIXLY_CORRECT。)

如果您使用带有 GNU-z标志的sed来处理 NUL 分隔的输入,例如 的输出find ... -print0,那么 NUL 将不会出现在模式空间中,而 NUL 是此处替换的不错选择。

尽管 NUL 不能在 bash 变量中,但可以将其包含在printf命令中。如果您的输入字符串可以包含任何字符,包括 NUL,那么请参阅Stéphane Chazelas 的答案,其中添加了一种巧妙的转义方法。


小智 11

sed - Christoph Sieghart 的非贪婪匹配

在 sed 中获得非贪婪匹配的技巧是匹配除终止匹配的字符之外的所有字符。我知道,这很简单,但我在上面浪费了宝贵的时间,毕竟 shell 脚本应该是快速和简单的。因此,万一其他人可能需要它:

贪心匹配

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar
Run Code Online (Sandbox Code Playgroud)

非贪婪匹配

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar
Run Code Online (Sandbox Code Playgroud)

  • “无脑”一词含糊不清。在这种情况下,不清楚您(或 Christoph Sieghart)是否考虑过这一点。特别是,如果您已经展示了如何解决问题中的特定问题(其中多个表达式的零后 * 跟 **多个字符**)*,那就太好了。您可能会发现此答案在这种情况下效果不佳。 (5认同)

Sté*_*las 9

一些sed实现对此提供支持。ssed有一个 PCRE 模式:

ssed -R 's/AB.*?AC/XXX/'
Run Code Online (Sandbox Code Playgroud)

AT&T ast sed支持*?运算符作为*其扩展(使用-E)和增强(使用正则表达式)的非贪婪版本-A。

sed -E 's/AB.*?AC/XXX/'
sed -A 's/AB.*?AC/XXX/'
Run Code Online (Sandbox Code Playgroud)

在该实现和那些-E/-A模式中,更一般地说,可以在 内部使用类似 perl 的正(?P:perl-like regexp here)则*?表达式,尽管如上所示,操作员不需要。

它的增广正则表达式也有合取和取反运算符:

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/'
Run Code Online (Sandbox Code Playgroud)

可移植地,您可以使用此技术:将结束字符串(此处AC)替换为在开头或结尾字符串中均未出现的单个字符(如此:处),以便您可以这样做s/AB[^:]*://,以防该字符可能出现在输入中, 使用不与开始和结束字符串冲突的转义机制。

一个例子:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'
Run Code Online (Sandbox Code Playgroud)

对于 GNU sed,一种方法是使用换行符作为替换字符。因为一次sed处理一行,换行永远不会出现在模式空间中,所以可以这样做:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/;s/\n/AC/g'
Run Code Online (Sandbox Code Playgroud)

这通常不适用于其他sed实现,因为它们不支持[^\n]. 对于 GNU,sed您必须确保未启用 POSIX 兼容性(如 POSIXLY_CORRECT 环境变量)。


Gil*_*il' 7

不,sed 正则表达式没有非贪婪匹配。

您可以AC通过使用“任何不包含的内容AC”后跟来匹配直到第一次出现的所有文本AC,这与 Perl 的.*?AC. 问题是,“任何不包含的东西AC”都不能轻易表达为正则表达式:总有一个正则表达式可以识别正则表达式的否定,但否定正则表达式会很快变得复杂。而在可移植的 sed 中,这根本不可能,因为否定正则表达式需要对存在于扩展正则表达式(例如在 awk 中)但不存在于可移植基本正则表达式中的交替进行分组。某些版本的 sed,例如 GNU sed,确实对 BRE 进行了扩展,使其能够表达所有可能的正则表达式。

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'
Run Code Online (Sandbox Code Playgroud)

由于否定正则表达式的困难,这不能很好地概括。你可以做的是暂时改变这条线。在某些 sed 实现中,您可以使用换行符作为标记,因为它们不能出现在输入行中(如果您需要多个标记,请使用换行符后跟不同的字符)。

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'
Run Code Online (Sandbox Code Playgroud)

但是,请注意反斜杠换行符在某些 sed 版本的字符集中不起作用。特别是,这在 GNU sed 中不起作用,它是非嵌入式 Linux 上的 sed 实现;在 GNU sed 中,您可以\n改用:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'
Run Code Online (Sandbox Code Playgroud)

在这种特定情况下,AC用换行符替换第一个就足够了。我上面介绍的方法更通用。

sed 中一种更强大的方法是将行保存到保持空间中,删除除第一个“有趣”部分之外的所有行,交换保持空间和模式空间或将模式空间附加到保持空间并重复。然而,如果你开始做这么复杂的事情,你真的应该考虑切换到 awk。awk 也没有非贪婪匹配,但您可以拆分字符串并将部分保存到变量中。