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


22

我想用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

2
这没有道理。您在Perl中有一个可行的解决方案,但是您想使用Sed,为什么呢?
库沙兰丹

Answers:


16

Sed正则表达式匹配最长的匹配项。Sed没有等效的非贪婪者。

显然我们想要做的是比赛

  1. AB
    其次
  2. 以外的任何数量AC
    其次是
  3. AC

不幸的是,sed不能做到第二点-至少对于多字符正则表达式来说不行。当然,对于单字符正则表达式(例如,@甚至[123]),我们可以执行[^@]*[^123]*。因此,我们可以通过更改ACto的所有出现@然后搜索来解决sed的局限性

  1. AB
    其次
  2. 以外的任何其他数字@
    后跟
  3. @

像这样:

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

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

但是,当然,这是一种鲁re的方法,因为输入内容可能已经包含@字符,因此,通过匹配它们,我们可能会得到误报。但是,由于任何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功能,用户必须未设置外壳程序变量POSIXLY_CORRECT。)

如果您使用带有GNU -z标志的sed 处理以NUL分隔的输入(例如的输出)find ... -print0,则NUL将不在模式空间中,在这里NUL是替代的好选择。

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


我刚刚编辑了您的答案以添加冗长的解释;随意修剪或回滚。
G-Man说'恢复莫妮卡'

@ G-Man这是一个很好的解释!做得非常好。谢谢。
John1024 '16

您可以echoprintf在bash的`\ 000'就好了(或输入可能来自文件)。但是总的来说,一串文本当然不会包含NUL。
ilkkachu '16

@ilkkachu你是对的。我应该写的是,任何外壳变量参数都不能包含NUL。答案已更新。
John1024 '16

这是不是可以整体安全很多,如果你改ACAC@,然后再返回?
Michael Vehrs's

7

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

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

使用增强型正则表达式时,AT&T ast sed具有连词和负号

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

可移植地,您可以使用此技术:将结束字符串(此处为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/g; # 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/g;s/\n/AC/g'

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


6

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

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

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

由于否定正则表达式很困难,因此不能一概而论。相反,您可以做的是临时更改线。在某些sed实现中,您可以使用换行符作为标记,因为它们不能出现在输入行中(如果需要多个标记,请使用换行符后跟可变字符)。

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

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

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

在这种情况下,AC用换行符替换第一个就足够了。我上面介绍的方法较为笼统。

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


@ilkkachu不,不是。s/\n//g删除所有换行符。
吉尔斯(Gillles)“所以-不要再邪恶了”

asdf。是的,我的坏。
ilkkachu

3

sed-Christoph Sieghart的非贪婪匹配

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

贪婪匹配

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

非贪婪匹配

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


3
术语“不费吹灰之力”是模棱两可的。在这种情况下,您(或克里斯托夫·西格哈特)是否对此深思熟虑还不清楚。特别是,它本来不错,如果你已经展示了如何解决这个问题(其中零的-更最先进表达之后的具体问题多于一个字符。您可能会发现此答案在这种情况下效果不佳。
斯科特,

兔子洞比乍看之下要深得多。没错,该解决方法不适用于多字符正则表达式。
gresolio

0

在您的情况下,您可以通过以下方式否定关闭char:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'

2
这个问题说:“我想更换第一之间的任何东西AB,并第一次出现ACXXX...”,并给出了ssABteAstACABnnACss作为例子输入。该答案适用于该示例,但通常不会回答该问题。例如,ssABteCstACABnnACss也应该产生output aaXXXABnnACss,但是您的命令使此行保持不变。
G-Man说'恢复莫妮卡'

0

解决方案非常简单。 .*是贪婪的,但不是绝对贪婪的。考虑匹配ssABteAstACABnnACssregexp AB.*AC。在AC随后.*必须实际有一个匹配。问题在于,由于.*贪婪,后一个AC将匹配最后一个 AC而不是第一个。 .*吃掉第一个,ACACregexp中的文字与ssABteAstACABnn AC ss 中的最后一个匹配。为防止这种情况发生,只需替换第一个AC可笑的东西,以区别于第二个和其他任何东西。

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

贪婪.*现在将停止在-foobar-in 的脚下,ssABteAst-foobar-ABnnACss因为-foobar-除了this之外没有其他了-foobar-,并且regexp -foobar- 必须有一个匹配项。先前的问题是正则表达式AC有两个匹配项,但是由于.*贪婪,AC因此选择了最后一个匹配项。但是,使用时-foobar-,只能进行一次匹配,并且该匹配证明.*并非绝对贪婪。对于的巴士站,.*在接下来的正则表达式中仅剩下一个匹配项.*

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


0

一种选择是更改字符串,以便您想要贪婪的匹配

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

使用rev扭转字符串,扭转你的匹配标准,使用sed以通常的方式,然后逆转的结果....

ssAB-+-+-+-+ACABnnACss
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.