在grep中使用正则表达式的“不跟随”先行断言

134
我试图使用grep查找所有不跟随另一个字符串的Ui\. 实例,甚至只是字母L
寻找特定字符串后面不跟随另一个字符串的正则表达式应该如何编写?
使用先行断言。
grep "Ui\.(?!L)" *
bash: !L: event not found


grep "Ui\.(?!(Line))" *
nothing

5
哪种正则表达式的子规范——PCRE、ERE、BRE、grep、ed、sed、perl、python、Java、C等? - Jonathan Leffler
5
顺带提一下,“event not found” 是由于使用历史扩展功能导致的。如果你从不使用该功能,有时需要在交互命令中使用感叹号,那么你可能需要关闭历史扩展功能。在Bash中可以使用“set +o histexpand”,在其他情况下可能会有所不同。 - tripleee
15
我也遇到了历史扩展问题。我认为通过改用单引号来解决它,这样shell就不会尝试篡改参数了。 - Coderer
@Coderer 使用单引号是很好的 - 除非您希望其他(通常是$)元字符处于活动状态。 ''保护所有元字符免受shell的影响,这只有在偶尔需要时才是您想要的。在我看来,tripleee的评论是处理此问题的最佳方法。 - Graham Nicholls
你希望其他元字符在正则表达式中内部活跃的频率有多高呢?如果你通过展开环境变量或其他方式动态构建正则表达式,那么你可能做错了。 - Coderer
6个回答

194

如果您需要使用负向前瞻,则需要比标准的 grep 更为强大的工具,需要启用 PCRE 的 grep。

如果您拥有 GNU grep,则当前版本支持选项-P--perl-regexp,然后您可以使用所需的正则表达式。

如果您没有(足够新的版本)GNU grep,那么可以考虑获取ack


45
在这种情况下,我很确定问题只是在bash中应该使用单引号而不是双引号,这样它就不会把!视为特殊字符。 - NHDaly
(请参见下面我描述的内容。) - NHDaly
4
经过核实,正确的答案应该是结合这个答案和@NHDaly的评论。例如,以下命令适用于我:grep -P '^.contains((?!but_not_this).)$' *.log.* >"D:\temp\result.out" - wangf
3
对于不支持-P选项的用户,请尝试再次将结果通过管道传递给grep --invert-match,例如:git log --diff-filter=D --summary | grep -E 'delete.*? src' | grep -E --invert-match 'xml'。请确保给@Vinicius Ottoni的回答点赞。 - Daniel Sokolowski
@wangf 我正在使用Cygwin下的Bash,当我改用单引号时,仍然出现“未找到事件”的错误。 - SSilk
显示剩余2条评论

47

你问题的一部分答案在这里,ack 的行为方式与此类似:Ack & negative lookahead giving errors

你正在使用双引号进行 grep,这允许 bash 将“!”解释为历史扩展命令。

你需要将模式用单引号括起来:grep 'Ui\.(?!L)' *

但是,请查看@JonathanLeffler's answer以解决标准 grep 中负向前瞻的问题!


你混淆了GNU grep的扩展功能和标准grep的功能,其中grep的标准是POSIX。你所说的也是正确的 - 我禁用了C-shell的野蛮行为来运行Bash(因为如果我想要一个C shell,我会使用它,但我不想要一个),所以!这些东西对我没有影响 - 但要获得负向先行断言,你需要非标准的grep - Jonathan Leffler
1
@JonathanLeffler,感谢您的澄清;我认为您是正确的,需要我们两个的答案来解决所有OP的症状。谢谢。 - NHDaly
1
通过在这个负向先行断言中使用 -E 选项,它会给出 grep: repetition-operator operand invalid 的错误提示 :( - Jerry Green

14

在grep中,您可能无法执行标准的负向后瞻,但通常可以使用“反向”开关“-v”来获得等效的行为。使用该开关,您可以构建一个用于匹配您想要排除内容的正则表达式,并将其通过两个grep进行传递。

对于所提到的正则表达式,您可以尝试像这样做:

grep 'Ui\.' * | grep -v 'Ui\.L'

(编辑:这不如真正的前瞻强,但通常可以用来解决问题。)


1
那将排除更多的事情,例如如果行包含Ui.Line和Ui但不包含.Line。 - nafg
1
是的,这就是为什么我不会严格规定它的原因。这只是解决了很大一部分导致人们遇到这个问题的情况,没有更多的东西。 - Karel Tucek
这个答案启发了我的最终解决方案,即使用 sed(在 busybox/alpine 上可用)将匹配的部分替换为空,即 grep 'match.+' | sed 's/match//' - lionello

7
如果你需要使用不支持负向先行断言的正则表达式实现,并且你不介意匹配额外的字符,那么你可以使用否定字符类[^L]或操作|字符串结尾锚点$

在你的情况下,grep 'Ui\.\([^L]\|$\)' *就能胜任。

  • Ui\.匹配你感兴趣的字符串

  • \([^L]\|$\)匹配除了L之外的任何单个字符,或者匹配行末:[^L]$

如果你想排除多于一个字符,那么你只需要添加更多的或操作和否定即可。要查找不跟随bca

grep 'a\(\([^b]\|$\)\|\(b\([^c]\|$\)\)\)' *

这个命令是匹配以下两种情况之一:(a后面不是b或者到了行尾:a然后是[^b]或者$) 或者 (a后面跟着b,后面不是c或者到了行尾:a然后是b,然后是[^c]或者$

这种表达式即使是对于一个简短的字符串也会变得非常笨拙和容易出错。你可以编写一些代码来生成这些表达式,但只要使用支持负向先行断言的正则表达式实现就可能更容易些。

*如果你的实现支持非捕获组,那么你可以避免捕获额外的字符。


3
如果您的grep不支持-P或--perl-regexp,并且您可以安装启用了PCRE的grep,例如“pcregrep”,那么它将不需要任何命令行选项(如GNU grep)来接受Perl兼容的正则表达式,您只需运行。
pcregrep "Ui\.(?!Line)"

在你的示例 "Ui.(?!(Line))" 中,你不需要另一个嵌套组来表示 "Line" -- 外部组已经足够了,就像我上面展示的那样。

让我再举一个查找否定断言的例子:当你有一个由 "ipset" 返回的行列表,每行显示中间的数据包数量,而你不需要零数据包的行时,只需运行:

ipset list | pcregrep "packets(?! 0 )"

如果您喜欢使用Perl兼容的正则表达式,同时拥有Perl语言但没有pcregrep命令或者您的grep命令不支持--perl-regexp参数,那么您可以使用一行Perl脚本来完成与grep相同的工作:
perl -e "while (<>) {if (/Ui\.(?!Lines)/){print;};}"

Perl像grep一样接受标准输入(stdin),例如:

ipset list | perl -e "while (<>) {if (/packets(?! 0 )/){print;};}"

3

至少对于不想在“Ui.”之后出现“L”字符的情况,您不需要使用PCRE。

    grep -E 'Ui\.($|[^L])' *

在这里,我已经确保匹配了行尾的"Ui."的特殊情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接