0%

Vim 实用技巧(07):查找 & 替换

这篇文章介绍 Vim 中查找和替换的相关技巧。

查找

技巧 79:结识查找命令

在普通模式下,按下 / 键会调出 Vim 的查找提示符,可以在它的后面输入要查找的模式或者原义文本。另外,只有我们按下回车键时,Vim 才会执行查找命令。如果使用 Esc 键的话,查找提示符就会想消失,重新回到普通模式。

默认情况下,Vim 会执行回绕查找:即到达文件尾部后,会继续从文件头部开始查找。可以通过关闭 wrapscan 选项来关闭回绕查找。

当使用 / 键进行查找时,Vim 将执行正向查找。而如果使用 ? 键进行查找时,Vim 将执行反向查找。

如果我们想要重复上一次查找,可以使用如下命令:

  • n:跳至下一处匹配,保持查找方向与偏移不变
  • N:跳至上一处匹配,保持查找方向与偏移不变
  • /<CR>:正向跳转到相同模式的下一处匹配
  • ?<CR>:反向跳转到相同模式的上一处匹配

后两个命令的原理是:如果不提供查找模式而直接进行查找的话,Vim 将会重用上一次查找模式。这使得我们可以在使用相同查找模式的情况下,直接改变查找方向或偏移。

当查找提示符出现时,可以通过 <UP> 键,滚动浏览之前的查找记录。

技巧 80:高亮查找匹配

通过启用 hlsearch 选项,Vim 可以将匹配高亮显示出来。当匹配数非常多时,高亮匹配可能反而会影响我们的判断,此时我们可以使用 :set nohlsearch 来彻底禁用高亮匹配功能。但是,下次我们需要高亮匹配时,又需要重新开启该选项。

Vim提供 :nohlsearch 命令来暂时关闭高亮匹配功能。该命令使得高亮功能一直处于关闭状态,直到执行新的或重复的查找命令为止。

技巧 81:在执行查找前预览第一处匹配

缺省情况下,在输入查找模式后,Vim 不会进行查找,只有按下回车后,查找才会开始。通过 incsearch 选项,Vim 会开启增量查找。此时 Vim 会根据已经在查找域中输入的文本,预览第一处匹配,而且该匹配会随着输入的变化而及时更新。通过这项功能,我们能及时知道是否已经查找到了我们的目标,而不必完整的输入查找模式。

如果我们只想知道是否存在某一匹配,而并不像真正跳到该匹配处,此时我们也可以通过 incserach 选项,如果该匹配被高亮显示后,我们可以按下 Esc 键取消查找,这样光标仍保持在原位。

incsearch 选项允许我们输入部分字符就查找我们的目标,但有时我们仍然希望能够完成的输入该查找模式(例如方便之后使用 substitue 命令),此时我们不必手动输入该模式,这里有个快捷键:<Ctrl-r><Ctrl-w>,其会根据当前匹配结果对查找域进行自动补全。

该快捷键有个注意事项:在输入模式中含有\v前缀时,会把光标下完整的单词作为补全内容,而不是单词余下的部分。

技巧 82:统计当前模式的匹配个数

通过如下命令可以统计某个模式的匹配个数:

1
:%s///gn

该命令实际是 :substitute 命令,其通过标志位 n 来抑制正常的替换动作,此时只是简单地统计匹配的次数。通过将查找域留空,Vim 会重用上次查找模式,而替换域始终会被忽略,因此也将其留空。

技巧 83:将光标偏移到查找匹配的结尾

可以用查找偏移将光标定位于距离某个匹配的起始或结尾一定步长的位置。当使用查找命令时,光标总定位于匹配的首字母。Vim的查找偏移功能,可以使得我们将光标定位于查找匹配的结尾。

在如下文本中:

1
2
3
Aim to learn a new programming language each year.
Which lang did you pick up last year?
Which langs would you like to learn?

如果想把 lang 替换为 language,一方面可以使用替换命令,另一方面可以使用点范式:

  • /lang/e:查找 lang 字符串,并将光标移动到匹配的结尾
  • auage<ESC>:添加字符串
  • n:调到下一处匹配
  • .:重复上次修改

之后,直接使用 n. 即可完成所有修改。

技巧 84:对完整的查找匹配进行操作

Vim 可以允许我们构造一条适用于所有匹配的动作命令。如果需要对如下文本

1
2
class XHTMLDocument < XMLDocument; end
class XHTMLTag < XMLTag; end

将其转换为如下形式:

1
2
class XHTMLDocument < XMLDocument; end
class XHTMLTag < XMLTag; end

可以通过如下命令完成这个:

  • /\vX(ht)?ml\C:构造查找表达式
  • gU//e:完成第一处修改
  • //:查找下一处
  • .:重复上次修改

之后反复通过 //. 即可重复完成修改。这里有个技巧://e<CR> 当做一个动作命令使用,其范围涵盖查找匹配的起始和结尾之间的全部内容。

注意,这里不能使用 n 键来调到下一处匹配,因为 n 键会使用上次的标志位,即跳到匹配的结束位置。因此必须通过 // 来跳转,之后才可以使用点范式重复修改。

技巧 85:利用查找历史,迭代完成复杂的模式

有时候,我们需要一套顺畅的工作流程,允许我们通过迭代的方式逐步完成模式的设计工作。

对于如下文本,如果我们要匹配所有用单引号括起来的文本,然后替换为用双引号括起来,我们首先要编写用于搜索的正则表达式。

1
2
3
This string contains a 'quoted' word.
This string contains 'two' quoted 'words.'
This 'string doesn't make things easy.'

如果使用如下正则表达式:

1
/\v'.+'

因为.+执行的是贪婪匹配,也就是匹配尽可能多的字符,所以该表达式不能符合预期。接下来如果使用如下正则表达式:

1
/\v'[^']+'

该匹配模式在执行到最后一行时会出现问题。这里,我们不需要重新出入查找模式,只需要按下 / 键,然后使用 <UP> 键即可翻看查找历史。

最后,我们输入如下正则表达式,得到最后正确的结果:

1
/\v'([^']|'\w)+'

这一次,在输入查找模式时,我们直接通过 q/ 来调出命令行窗口。该窗口与常规的 Vim 缓冲区类似,只不过其内容是查找历史。之后我们就可以通过 Vim 强大的分模式编辑功能来修改上次的正则表达式了。得到了理想的模式后,只需要按下回车键即可查找。

最后,我们通过如下命令完成替换动作。这里通过额外的 () 来捕获子匹配,并将结果保存到 \1 捕获寄存器中。

1
:%s/\v'(([^']|'\w)+)'/"\1"/g

技巧 86:查找当前高亮选区中的文本

* 命令使得我们可以查找光标下的单词。通过一段 Vim 脚本,我们可以重新定义可视模式下的 * 命令,使其查找当前选中的文本。

在没有特殊配置的情况下,在可视模式下使用 * 命令将查找光标下的单词,并将高亮选区范围扩大到下一处匹配。但该功能没有多大作用。我们更希望 * 命令能够查找当前选中的文本。

通过如下脚本,可以实现这一功能(:h visual-search):

1
vmap X y/<C-R>"<CR>

其建立一个键盘映射 X,然后复制当前选中的文本,并调出查找符,然后复制选中的文本(访问无名寄存器 "),最后执行搜索。

替换

substitute 是最强大的 Ex 命令之一。

技巧 87:结识 substitute 命令

substitute 命令允许我们先查找一段文本,然后再用一段文本将其替换掉。命令语法如下:

1
:[range]s[ubstitute]/{pattern}/{string}/[flags]
  • range 规则适用于每一条 Ex 命令,包括 substitute
  • pattern 则为搜索模式
  • string 为要替换的字符串
  • flags 为标志位

可以用 flags 标志位来调整 substitute 命令的行为(:h s_flags):

  • 标志位 g:使得 substitute 命令修改一行内的所有匹配,而不仅仅是第一处匹配
  • 标志位 c:使得我们确认或拒绝每一处修改
  • 标志位 n:抑制正常的替换行为,仅仅报告匹配次数
  • 标志位 e:当查找不到匹配模式时,Vim会提示错误信息,标志位 e 可以屏蔽这些错误提示
  • 标志位 &:Vim 重用上一次 substitute 命令所用过的标志位

在查找模式中,一些字符具有特殊含义,而替换域也有一些特殊字符(:h sub-replace-special):

  • \r:插入换行符
  • \t:插入制表符
  • \:插入反斜杠
  • \1:插入第一个子匹配
  • \2:插入第二个子匹配
  • \0:插入匹配的所有内容
  • &:插入匹配模式的所有内容
  • ~:上一次调用 substitute 时的 {string}
  • ={vim script}:执行 vim script 表达式,并将结果作为替换域中的 {string} 使用

技巧 88:在文件范围内查找并替换每一处匹配

默认情况下,substitute 命令仅作用于当前行,而且只会修改第一处匹配。如果我们想修改整个文件内的所有匹配,需要使用如下表达式:

1
:%s/pattern/string/g
  • substitute 命令只是 ex 命令之一,所以也可以使用 % 来代表文件所有行
  • 而最后的标志位 g,则代表替换一行内的所有匹配,而不仅仅是第一处匹配

因此,通过该命令就就可以替换文件内的所有匹配。

技巧 89:手动控制每一次替换操作

在使用 substitute 命令进行替换时,如果使用标志位 c,Vim 会对每处匹配结果进行提示。如下所示:

1
replace with that (y/n/a/q/l/^E/^Y)?

对于提示,我们可以通过如下应答控制 Vim 的行为:

  • y:替换此处匹配
  • n:忽略此处匹配
  • q:退出替换过程
  • l:替换此处匹配后退出
  • a:替换此处与之后的所有匹配
  • <Ctrl-e>:向上滚动屏幕
  • <Ctrl-y>:向下滚动屏幕

另外需要注意,在 Vim 的 替换-确认 模式下,键盘上的大多数按键都将失效。

技巧 90:重用上一次的查找模式

执行 substitute 命令通常包含两个步骤:一是撰写查找模式,二是设计合适的替换字符串。通过一分为二的技术让我们消除了这两项任务的耦合性。因此我们可以先通过编写查找模式,直到正确后(查找模式可能需要反复尝试)再编写替换命令。

substitute 命令的查找域留空,Vim 将会重用上一次的查找模式。因此当上一次的查找模式符合我们要求后,我们可以将查找域留空,Vim 就会使用上一次的查找模式。当然简单的替换命令就没有必要了。

当然,有一点需要注意:把查找域留空会在命令历史中留下一项不完整的记录。因为模式通常保存在 Vim 的查找历史记录中,而 substitute 命令则保存在 ex 命令历史记录中。因此将查找任务与替换任务分离,会使这两组信息单独保存。

如果需要解决这个问题,可以将查找域填充完整,但是不必手动输入,可以通过 <Ctrl-r>/ 将上次的查找内容粘贴进来。

技巧 91:用寄存器的内容替换

通过输入 Ctrl-r>{register} 之后,可以将寄存器的内容插入到命令行中。由于文本中可能存在一些特殊字符,导致我们需要转义这些字符,为了避免这个问题,可以直接引用寄存器中的内容:\=@{register}

  • \= 将执行 Vim 执行一段表达式脚本
  • 在 Vim 脚本中,可以用 @register 来引用某个寄存器的内容

技巧 92:重复上一次 substitue 命令

如果我们在某一行执行了替换命令:

1
:s/target/replacement/g

如果想要在整个文件范围内重复这条命令,可以直接使用 g& 命令(:h g&,它等效于:

1
:%s//~/&

这条命令可以理解为:用同样的标志位、同样的替换字符串、同样的查找模式以及新的执行范围 % 来重复上一次 substitute 命令。

还有一个命令值得说明,即 :&&,这两处 & 符号的含义有所不同:

  • 前一个 & 作为 Ex 命令 :& 的组成部分,用作重复上一次 :substitute 命令
  • 而第二个 & 则会重用上一次 :s 命令的标志位

我们总是可以指定一个新的范围,并会用 :&& 命令重新执行替换操作,而可以把 g& 命令作为 :%&& 的快捷方式。

技巧 93:使用子匹配重排 CSV 文件的字段

这里会介绍如何从查找模式中捕获子匹配,并在替换域中引用它们。如果存在如下 CSV 文件:

1
2
3
last name,first name,age
jack,ma,20
jonh,li,30

如果需要改变这三列的次序,可以使用如下命令:

1
2
/\v^([^,]*),([^,]*),([^,]*)$
:%s//\3,\2,\1/g

这里我们使用了子匹配来匹配每列内容,之后在替换域中通过通过 \n 的方式来引用这些子匹配。

技巧 94:在替换过程中执行算术运算

替换域中的内容不一定非得是简单的字符串。我们可以执行一段 Vim 脚本表达式,然后用其结果充当替换字符串使用。例如对于如下文本,如果想将所有的 HTML 层级增加 1:

1
2
3
<h2>Heading number 1</h2>
<h3>Number 2 heading</h3>
<h4>Another heading</h4>

使用如下模式可以匹配到 h2、h3 等字符串中的数字:

1
/\v\<\/?h\zs\d

接下来在 subsutitute 命令的替换域中执行算术运算,这里需要执行一段 Vim 脚本。在 Vim 中,通过调用函数 submatch(0) 就可以得到匹配内容:因此通过如下替换命令即可完成该任务:

1
:%s//\=submatch(0)-1/g

技巧 95:交换两个或更多的单词

通过使用表达式寄存器以及 Vim 脚本中的字典数据结构(dictionary),我们可以设计一条特殊的 substitute 命令,用它来对两个单词进行交互。

假设需要将如下文本中的 man 和 dog 单词交换

1
the dog bite the man

如下创建一个字典数据结构,其中包含两对 键值对

1
2
3
4
5
:let swapper={"dog":"man", "man":"dog"}
:echo swapper["dog"]
> man
:echo swapper["man"]
> dog

首先通过如下模式查找 man 或者 dog 单词:

1
/\v(man|dog)

接下来通过如下命令进行替换,这里将查找域留空,从而重用上次的查找模式。在替换域中我们通过 \= 开启一段 Vim 脚本。

1
:%s//\={"man":"dog","dog":"man"}[submatch(1)]/g

技巧 96:在多个文件中执行查找和替换

substitute 命令通常只针对当前文件进行操作,如果想要在整个工程范围内实现形同的替换操作,需要通过组合一些命令来实现。例如通过如下 :args 命令打开所有文件,之后在通过 :argdo 在每个文件上执行替换操作:

1
2
:args **/*
:argdo %s/test/Test/g

上述命令的缺点是会在所有文件上执行 substitute 命令,即使该文件没有指定的匹配模式。如下 vimgrep 命令在当前文件夹的所有文件内进行查找:

1
:vimgrep /test/ **/*

由于 vimgrep 返回的匹配都将在 quickfix 列表中被记录下来,通过运行 :copen 可以打开 quickfix 窗口。借助一些 Vim 脚本的支持,我们可以将 quickfix 列表中的文件都加载到参数列表中,这样就可以只对的确存在指定模式的文件执行 substitute 命令了。