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

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

我想用它sed来替换第一个之间的字符串中的任何内容AB和第二个之间的任何内容第一的的出现AC(包括)XXX

为了例子,我有这个字符串(这个字符串仅用于测试):

ssABteAstACABnnACss

我想要类似于这样的输出:ssXXXABnnACss


我用以下方法做到了这一点perl

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

但我想用 来实现它sed。以下(使用 Perl 兼容的正则表达式)不起作用:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss

答案1

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'

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

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

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

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

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

尽管 NUL 不能出现在 bash 变量中,但可以将其包含在printf命令中。如果您的输入字符串可以包含任何字符,包括 NUL,那么请参阅斯特凡·查泽拉斯的回答这增加了一个巧妙的转义方法。

答案2

进行非贪婪匹配单个字符,匹配除终止匹配的字符之外的所有字符。

贪心匹配:

$ echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

非贪婪匹配:

$ echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

来源:sed - Christoph Sieghart 的非贪婪匹配

答案3

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

ssed -R 's/AB.*?AC/XXX/'

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

sed -E 's/AB.*?AC/XXX/'
sed -A 's/AB.*?AC/XXX/'

在该实现和那些-E/-A模式中,更一般地,可以在 内部使用类似 perl 的正则表达式(?P:perl-like regexp here),尽管如上所示,这对于运算符来说不是必需的*?

它是增强的正则表达式还具有合取和否定运算符:

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/'

可移植的是,您可以使用此技术:将结束字符串(此处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'

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

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/;s/\n/AC/g'

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

答案4

解决方案非常简单。 .*是贪婪的,但不是绝对贪婪的。考虑ssABteAstACABnnACss与 regexp匹配AB.*ACAC接下来的内容实际上.*必须有一个匹配。问题是因为.*贪婪,后续AC将匹配最后的 AC而不是第一个。 .*吃掉第一个AC,而正则表达式中的文字AC与 ssABteAstACABnn 中的最后一个匹配交流电SS。为了防止这种情况发生,只需AC用一些东西替换第一个荒谬的将其与第二个以及其他任何东西区分开来。

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

贪婪现在将停在in.*的脚下,因为除了这个和正则表达式之外没有其他的了-foobar-ssABteAst-foobar-ABnnACss-foobar--foobar--foobar- 必须来一场比赛吧。之前的问题是正则表达式AC有两个匹配项,但由于.*贪心,所以AC选择了最后一个匹配项。然而,对于-foobar-,只有一个匹配是可能的,并且这个匹配证明这.*不是绝对贪婪的。巴士站.*仅出现在后面的正则表达式的其余部分仍然匹配.*

AC请注意,如果出现在第一个之前,此解决方案将失败,AB因为错误AC将被替换-foobar-。例如,第一次sed替换后,ACssABteAstACABnnACss变为-foobar-ssABteAstACABnnACss;因此,找不到 的匹配项AB.*-foobar-。但是,如果序列始终为 ...AB...AC...AB...AC...,则此解决方案将成功。

相关内容