如何使用Vim在文件中提取所有正则表达式匹配项?

22

考虑下面的例子:

case Foo:
    ...
    break;
case Bar:
    ...
    break;
case More: case Complex:
    ...
    break:
...

假设我们想要检索符合正则表达式case \([^:]*\):的所有匹配项(整个匹配文本或者更好的是位于\(\)之间的部分),这应该给我们(最好在新缓冲区中)类似于下面这样的东西:

Foo
Bar
More
Complex
...

另一个用例示例是从HTML文件中提取一些片段,例如图像URL。

是否有一种简单的方法,在Vim中收集所有正则表达式匹配项并将它们放入单独的缓冲区中?

注意:这与问题“如何使用Vim提取与正则表达式匹配的文本?”类似。但与该问题中的设置不同,我还想删除不匹配的行,最好不要使用非常复杂的正则表达式。


2
你是指反向引用吗?:%s/^\vcase ([^:]+):/\1/ 使用\1来获取第一个捕获组。 - mathematical.coffee
如果你只是想将它们提取到一个新文件中(从你的问题中不太清楚),你可以使用sed或grep更容易地完成这个任务;sed示例:sed -n '/^\s*case\s\+/{s/\s*case\s\+\([^:]\+\):/\1/;p}' file - beerbajay
@beerbajay:是的,在新文件中完全可以。 我同意使用sed会很好,只是我需要打开终端并再次查找文件,因此我正在寻找Vim解决方案。 - Wernight
@mathematical.coffee:完全不是。问题不在于搜索和替换(除非包括换行符),而在于获取所有匹配项并将它们放入另一个缓冲区中。 - Wernight
1
这与此问题非常相似:https://dev59.com/bcF2zogBFxS5KdRjOHw4#4521486 - Peter Rincker
@PeterRincker:你说得对。问题的表述方式不同,但目标基本相同。看起来没有“简单”的答案。:( - Wernight
5个回答

32

通过利用:substitute命令中的表达式替换功能(参见:help sub-replace-\=),有一种在文本中收集模式匹配的通用方法。关键思想是使用一个替换枚举所有的模式匹配来计算一个表达式,将它们存储而不进行替换。

首先,让我们考虑保存匹配项。为了保留一系列匹配的文本片段,使用列表(参见:help List)很方便。然而,不能直接使用:let命令修改列表,因为没有办法在表达式中运行Ex命令(包括\=替换表达式)。但是,我们可以调用其中一个能够就地修改列表的函数,例如,将给定项附加到列表的add()函数(参见:help add())。

另一个问题是如何在运行替换时避免文本修改。一种方法是始终使模式具有零宽度匹配,方法是在其前面加上\ze或在其后面添加\zs原子(参见:help /\zs:help /\ze)。以这种方式修改的模式捕获了原始模式在文本中前面或后面的空字符串位置(这些匹配在Vim中称为零宽度匹配,参见:help /zero-width)。然后,如果替换文本也为空,则替换实际上不会改变任何内容:它只是用一个空字符串替换了零宽度匹配。

由于add()函数和大多数修改列表的函数一样,返回更改后列表的引用,因此为了使我们的技术起作用,我们需要以某种方式从中获取一个空字符串。最简单的方法是通过指定索引的范围来从中提取长度为零的子列表,其中起始索引大于结束索引。

结合上述想法,我们得到以下Ex命令:

:let m=[] | %s/\<case\s\+\(\w\+\):\zs/\=add(m,submatch(1))[1:0]/g

执行后,第一子组的所有匹配都将累积在变量m引用的列表中,并且可以直接使用或以某种方式进行处理。例如,在插入模式下逐行粘贴列表内容的方法如下:

Ctrl+R=mEnter

要在普通模式下执行相同操作,只需使用:put命令即可:

:put=m

从7.4版本开始(参见:helpg Patch 7.3.627),Vim会在替换命令的替换字符串中的每个匹配项上评估一个\=表达式,即使有n标志(该标志指示Vim仅计算匹配项的数量而不进行替换-请参见:help :s_n)。在这种情况下,表达式的求值结果并不重要,因为由于在计数过程中没有进行替换,所以最终结果会被丢弃。

这使我们能够利用表达式的副作用,而无需担心在该过程中保留缓冲区的内容,因此可以省略所有零宽度匹配和空子列表索引的技巧:

:let m=[] | %s/\<case\s\+\(\w\+\):/\=add(m,submatch(1))/gn

方便的是,运行此命令后,该缓冲区甚至不会被标记为已修改。


不错的回答。我特别喜欢在替换表达式中使用extend()的小技巧。 - Herbert Sitz
@HerbertSitz:谢谢,我刚刚注意到可以使用add()函数而不是extend()。顺便说一下,我已经重写了答案以更详细地解释这个技巧。 - ib.
1
不错的技巧。由于替换会设置“modified”标志,因此我们可以选择让add()返回上次添加的元素[-1]。 这样可以避免进行零宽度匹配和捕获: :let t=[] | %s/\<case\s\+\(\w\+\):/\=add(t,submatch(0))[-1]/g - Ingo Karkat
@Ingo:但这样我们最终得到的列表会包含case Foo:case Bar:等,而不是所需的FooBar等。看来我们无论如何都不能在不使用\zs\ze更改匹配边界的情况下正确解决问题。 - ib.

3
虽然无法编写一行代码来完成您的示例,但是在交互式地键入命令,例如:%s / case \([^:] * \):/ \ = ... / 很困难。

我更喜欢使用vim-grex,具体步骤如下:

  1. 使用/检查正则表达式是否与预期行匹配。 例如:/^\s*\<case\s\+\([^:]*\):.*$<Enter>
  2. 执行:Grey。它会将当前搜索模式匹配的行复制到剪贴板。
  3. 通过:new等打开一个新缓冲区。
  4. 通过p等将复制到剪贴板的行粘贴到新缓冲区中。
  5. 通过:%s // \ 1 / 修剪不感兴趣的部分。

2
如何使用vim正则表达式从以下行中提取单词,假设“help”可能是任何单词,如“rust”或“perlang”。
vim:tw=78:ts=8:ft=help:norl:

解决方案:

let foo = substitute(foo, '^\s*vim:.*:ft=\([a-z]\+\).*:\s*$', '\1', '')
echo "foo: '" . foo . "'"

输出:

foo: 'help'

大师冥想:这里发生了什么?

取变量foo中的字符串并匹配,以断言行的开头,然后是任意数量的空格,字面上的vim和一个字面上的冒号,然后是任意数量的任何字符,后跟冒号ft=与任何具有字母的单词,然后是任何内容,并断言该行以冒号结束。将所有内容放入名为1的寄存器中,然后在参数2中获取它,substitute使用它并替换前面的字符串。

作为一般原则,任何比屏幕上的手指更长的正则表达式都是失败的,因此请降低屏幕分辨率直到适合为止。


1
作为对ib.已经给出的答案的补充,它本身就很好。似乎标志足以避免不必要的替换问题。
:let t=[] | %s/\<case\s\+\(\w\+\):/\=add(t,submatch(1))/gn

来自 s_flag 帮助文档:

[n] 报告匹配次数,不实际替换。[c] 标志被忽略。如果 'report' 为零,则报告的匹配次数将与实际匹配次数相同。适用于计数项目。如果使用 \= sub-replace-expression,则表达式将在每次匹配时在沙盒中进行评估。


我在查找其他内容时,偶然发现了扫描:help :s_flagsn标志的行为!在回去更新我的答案以利用这个特性后,我注意到你自那时以来已经发现了它。很棒能够抓住这一点! - ib.
事实证明,它是在Vim 7.4的开发过程中引入的(请参见:helpg Patch 7.3.627),当我写原始答案时,它还不存在(它在八个月后的2012年8月提交到Vim存储库,并在一年后的2013年8月随版本7.4发布)。我希望我早点知道它。 - ib.

0
:g/^case\s\L\l\+\scase.*/s/case/\r&/g
:let @a=''|g/^case\s\L\l\+:/y A

现在打开一个新的缓冲区或临时文件,并应用以下操作:
"ap
:%s_^\vcase ([^:]+):_\1_

或者如果您不关心当前的缓冲区(当然可以撤消此操作)(针对复杂示例进行了更新):

:g/^case\s\L\l\+\scase.*/s/case/\r&/g
:v/^case\s\L\l\+:/d
:%s_^\vcase ([^:]+):_\1_

3
第一个代码片段中列出的命令中肯定存在一些错误。您在发布前运行过它们吗?这两个命令都无法运行!您可能想表达的是类似于:let@a=''|g/^case\s\L\l\+:/y A的内容。 - ib.
:v/.../d:g!/.../d 是一个不错的技巧,它可以删除所有不匹配的行。然而,它并没有真正执行正则表达式匹配。它提取匹配的行,然后假设每行只有一个匹配项,第二次搜索和替换才能起作用。在一般情况下,它是无法工作的。我会更新我的示例。 - Wernight
@ib,感谢您指出这一点,您是正确的。当我在Windows系统上,在Excel前面时会发生这种情况...正在更新答案。 - Zsolt Botykai
@Wernight,好的,我已经针对你的特殊情况更新了我的答案。 - Zsolt Botykai

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