这对于单个正则表达式或s/// 正则表达式替换是不可能的,除非解释器支持动态宽度后向。
我将在 vim 中解决这个问题,它的正则表达式解释器实际上支持动态后视,但它真的很迟钝,所以我将首先重新创建 delete-first-instance 变体(问题中的^(\w+)\R(?=.*?^\1$))。
:%s/^\(\w\+\)\n\ze\%(^\w\+\n\)*\1$//ig vim 命令 (:) 将对所有行 (%) 使用替换 (s/…//ig) 来删除以 (@987654327 开头的行的正则表达式的不区分大小写的全局匹配) @) 1+ 个单词字符 (\w\+) 的捕获 (\(…\)),后跟换行符 (\n)。比赛的其余部分是零宽度前瞻(\ze 表示“零宽度结束”,\zs… 类似于 PCRE 正则表达式末尾的(?=…))。然后,在匹配原始捕获 (\1) 之前,我们会跳过零个或多个非捕获组 (\%(…\)*),它们在自己的行中包含单词。由于\ze,当我们删除第一个实例时,该部分不会被删除,给我们留下:
TEST
bananA
Var
applE
cherrY
(我讨厌写 vimscript 和 vim 正则表达式。我真的不知道你是怎么说服我的……)
这是一个可以接受的解决方案。 (我之所以这么说是因为/g 不够全球化。)
:%s/^\(\w\+\)\n\%(\w\+\n\)*\zs\1\n//ig 使用与前面的 delete-first-instance 命令非常相似的组合。我已将 \ze 更改为 \zs(“零宽度开始”,如 PCRE \K)。这实际上是一个可变宽度的后视。 (是的,理论上我可以用 vim 的\%(…\)\@<= 让它看起来更像(?<=…),但这更丑,我无法让它工作。)那个“跳过”组被移动到保持在零宽度的一侧。
尽管宽度为零,但每次替换都需要运行一次(在本例中为 4 次)。我相信这是因为匹配是在最后一个实例上设置的,所以每次替换都必须消耗空间直到最后匹配(这个正则表达式是贪婪的)然后后退,但是在第一次替换之后,它不知道迭代倒退到下一个捕获。
四次运行后,您将得到:
Apple
Banana
TEST
Cherry
Var
(是的,这是一个拖尾的空白行。这可能是在同一操作中同时删除 apple 和 cherrY 的产物。)
这是一个使用 Javascript 的更实用的解决方案,使用正则表达式完成尽可能多的工作:
test = "Apple\nBanana\nTEST\napple\nCherry\nbanana\nbananA\nVar\ncherry\ncherrY\n";
while ( test != ( test = test.replace(/^(\w+\n)((?:\w+\n)*)\1/mig, "$1$2") ) ) 1;
所有逻辑都存在于while 循环的条件内,它基本上是通过比较替换前的字符串 (!=) 与替换后的字符串来表示“执行此替换并循环直到它不做任何事情”替换。循环意味着我们不必处理零宽度,因为我们在每次迭代时重新开始(否则正则表达式会从它停止的地方恢复,因此需要零宽度)。
正则表达式本身只是在自己的行中捕获一个单词 (^(\w+\n)),然后匹配零个或多个其他单词 (((?:\w+\n)*)),然后再次捕获的单词 (\1)。
while 循环的主体为空(1 是空操作),因为条件包含所有逻辑。 Javascript 在给出单个命令时不需要大括号,但更正式的形式是
while ( test != ( test = test.replace(…) ) ) { true; }
此循环四次(您可以通过在循环前设置i=0 并将循环内的1 更改为i++ 来计数)然后将test 保留为:
Apple
Banana
TEST
Cherry
Var