声明

本文搬运自Github仓库Learn-Vim_zh_cn,并针对部分错别字做出修正。

本章涵盖两个独立但相关的概念:搜索和替代。很多时候,您得基于文本的共同模式搜索大量的内容。通过学习如何在搜索和替换中使用正则表达式而不是字面字符串,您将能够快速定位任何文本。

附带说明一下,在本章中,当谈论搜索时,我将主要使用/。您使用/进行的所有操作也可以使用?完成。

智能区分大小写

尝试匹配搜索词的大小写可能会很棘手。如果要搜索文本"Learn Vim",则很容易把字母的大小写输错,从而得到错误的搜索结果。如果可以匹配任何情况,会不会更轻松,更安全?这是选项ignorecase闪亮的地方。只需在 vimrc 中添加set ignorecase,所有搜索词就不区分大小写。现在,您不必再执行/Learn Vim了。 /learn vim将起作用。

但是,有时您需要搜索特定大小写的短语。一种方法是用 set noignorecase 关闭ignorecase选项,但是每次需要搜索区分大小写的短语时,都得反复地打开和关闭这个选项。

为避免反复开关ignorecase选项,Vim 有一个smartcase选项。您可以将ignorecasesmartcase选项结合起来,当您输入的搜索词全部是小写时,进行大小写不敏感搜索;而当搜索词 至少有1个大写字母时,进行大小写敏感搜索。

在您的 vimrc 中,添加:

1
set ignorecase smartcase

如果您有这些文字:

1
2
3
hello
HELLO
Hello
  • /hello 匹配"hello",“HELLO"和"Hello”。
  • /HELLO 仅匹配"HELLO"。
  • /Hello 仅匹配"Hello"。

有一个缺点。因为现在当您执行/hello时,Vim 将进行大小写不敏感搜索,那如果只需要搜索小写字符串怎么办?您可以在搜索词前使用\C模式来告诉 Vim,后续搜索词将区分大小写。如果执行/\Chello,它将严格匹配"hello",而不是"HELLO"或"Hello"。

一行中的第一个和最后一个字符

您可以使用^匹配行中的第一个字符,并使用$匹配行中的最后一个字符。

如果您有以下文字:

1
hello hello

您可以使用/^hello来定位第一个"hello"。 '^'后面的字符必须是一行中的第一个字符。 要定位最后一个"hello",请运行/hello$。 ‘$’ 之前的字符必须是一行中的最后一个字符。

如果您有以下文字:

1
hello hello friend

运行/hello$将匹配不到任何内容,因为"friend"是该行的最后一项,而不是"hello"。

重复搜索

您可以使用//重复上一个搜索。如果您只是搜索/hello,则运行//等同于运行/hello。此快捷键可以为您节省一些按键操作,尤其是在您刚搜索了一个很长的字符串的情况下。另外,回想一下前面的章节,您还可以使用nN分别以相同方向和相反方向重复上一次搜索。

如果您想快速回忆起 第n个最近使用的搜索字怎么办?您可以先按/,然后按up/down方向键(或Ctrl-N/Ctrl-P),快速遍历搜索历史,直到找到所需的搜索词。要查看所有搜索历史,可以运行:history /

在搜索过程中到达文件末尾时,Vim 会抛出一个错误:"搜索到达底部,未找到匹配项:{your-search}"("Search hit the BOTTOM without match for: {your-search}")。有时这个特性能成为一个安全守卫,可以防止过度搜索,但是有时您又想将搜索重新循环到顶部。您可以使用set wrapscan选项使 Vim 在到达文件末尾时回到文件顶部进行搜索。要关闭此功能,请执行set nowrapscan

使用候选词搜索

一次搜索多个单词属于日常操作。 如果您需要搜索"hello vim"或"hola vim",而不是"salve vim"或"bonjour vim",则可以使用|或运算符。

给予这样一段文本:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

要同时匹配"hello"和"hola",可以执行/hello\|hola。 您必须使用(\)转义(|)或运算符,否则 Vim 将按字面意义搜索字符串"|"。

如果您不想每次都输入\|,则可以在搜索开始时使用magic语法(\v):/\vhello|hola。 我不会在本章中详细介绍magic,但是有了\v,您就不必再转义特殊字符了。 要了解有关\v的更多信息,请随时查看:h \v

设置模式匹配的开始位置和结束位置

也许您需要搜索的文本是复合词的一部分。如果您有这些文字:

1
2
3
4
11vim22
vim22
11vim
vim

如果您仅需要选择以"11"开始、以"22"结束的"vim",您可以使用\zs(开始匹配)和\ze(结束匹配)运算符。 执行:

1
/11\zsvim\ze22

Vim仍然会匹配整个模式"11vim22",但是仅高亮显示介于\zs\ze之间的内容。 另一个例子:

1
2
foobar
foobaz

如果需要在"foobaz"中搜索"foo",而不是在"foobar"中搜索,请运行:

1
/foo\zebaz

搜索字符组

到目前为止,您所有的搜索字都是字面内容。在现实生活中,您可能必须使用通用模式来查找文本。最基本的模式是字符组[ ]

如果您需要搜索任何数字,则可能不想每一次都输入/0\|1\|2\|3\|4\|5\|6\|7\|8\|9\|0。相反,请使用/[0-9]来匹配一位数字。 0-9表达式表示 Vim 尝试匹配的数字范围是 0-9,因此,如果要查找 1 到 5 之间的数字,请使用/[1-5]

数字不是 Vim 可以查找的唯一数据类型。您也可以执行/[a-z]来搜索小写字母,而/[A-Z]来搜索大写字母。

您可以将这些范围组合在一起。如果您需要搜索数字 0-9 以及从 a 到 f(十六进制)的小写字母和大写字母,可以执行/[0-9a-fA-F]

要进行否定搜索,可以在字符范围括号内添加^。要搜索非数字,请运行/[^0-9],Vim会匹配任何字符,只要它不是数字即可。请注意,范围括号内的脱符号(^)与行首位置符号(例如:/^hello)不同。如果插入号在一对方括号之外,并且是搜索词中的第一个字符,则表示"一行中的第一个字符"。如果插入符号在一对方括号内,并且是方括号内的第一个字符,则表示否定搜索运算符。 /^abc匹配行中的第一个"abc",而/[^abc]匹配除"a","b"或"c"以外的任何字符。

搜索重复字符

如果需要在此文本中搜索两位数:

1
2
3
1aa
11a
111

您可以使用/[0-9][0-9]来匹配两位数字字符,但是该方法难以扩展。 如果您需要匹配二十个数字怎么办? 打字 20 次[[0-9]]并不是一种有趣的体验。 这就是为什么您需要一个count参数。

您可以将count传递给您的搜索。 它具有以下语法:

1
{n,m}

顺便说一句,当在 Vim 中使用它们时,这些count周围的花括号需要被转义。 count 运算符放在您要递增的单个字符之后。

这是count语法的四种不同变体:

  • {n}是精确匹配。 /[0-9]\{2\}匹配两个数字:“11”,以及"111"中的"11"。
  • {n,m}是范围匹配。 /[0-9]\{2,3\}匹配 2 到 3 位数字:“11"和"111”。
  • {,m}是上限匹配。 /[0-9]\{,3\}匹配最多 3 个数字:“1”,“11"和"111”。
  • {n,}是下限匹配。 /[0-9]\{2,\}匹配最少 2 个或多个数字:“11"和"111”。

计数参数\{0,\}(零或多个)和\{1,\}(一个或多个)是最常见的搜索模式,Vim 为它们提供了特殊的操作符:*++需要被转义,而* 可以正常运行而无需转义)。 如果执行/[0-9]*,功能与/[0-9]\{0,\}相同。 它搜索零个或多个数字,会匹配"“,“1”,“123”。 顺便说一句,它也将匹配非数字,例如"a”,因为在技术上,字母"a"中的数字个数为零。 在使用"*“之前,请仔细考虑。 如果执行/[0-9]\+,则与/[0-9]\{1,\}相同。 它搜索一个或多个数字,将匹配"1"和"12”。

预定义的字符组

Vim 为常见字符组(例如数字和字母)提供了简写。 我不会在这里逐一介绍,但可以在:h /character-classes中找到完整列表。 下面是有用的部分:

1
2
3
4
5
6
7
\d    数字[0-9]
\D 非数字[^ 0-9]
\s 空格字符(空格和制表符)
\S 非空白字符(除空格和制表符外的所有字符)
\w 单词字符[0-9A-Za-z_]
\l 小写字母[a-z]
\u 大写字符[A-Z]

您可以像使用普通字符组一样使用它们。 要搜索任何一位数字,可以使用/\d以获得更简洁的语法,而不使用/[0-9]

搜索示例:在一对相似字符之间捕获文本

如果要搜索由双引号引起来的短语:

1
"Vim is awesome!"

运行这个:

1
`/"[^"]\+"`

让我们分解一下:

  • " 是字面双引号。它匹配第一个双引号。
  • [^"] 表示除双引号外的任何字符,只要不是双引号,它就与任何字母数字和空格字符匹配。
  • \+表示一个或多个。因为它的前面是[^"],因此 Vim 查找一个或多个不是双引号的字符。
  • " 是字面双引号。它与右双引号匹配。

当看到第一个"时,它开始模式捕获。Vim 在一行中看到第二个双引号时,它匹配第二个"模式并停止模式捕获。同时,两个双引号之间的所有非双引号字符都被[^"]\+ 模式捕获,在这个例子中是短语"Vim is awesome!"。这是一个通用模式(其实就是正则表达式)用于捕获 由一对类似的定界符包围的短语

  • 要捕获被单引号包围的短语,你可以使用/'[^']\+'
  • 要捕获为0包围的短语,你可以使用/0[^0]\+0

搜索示例:捕获电话号码

如果要匹配以连字符(-)分隔的美国电话号码,例如123-456-7890,则可以使用:

1
/\d\{3\}-\d\{3\}-\d\{4\}

美国电话号码的组成是:首先是三位数字,其后是另外三位数字,最后是另外四位数字。 让我们分解一下:

  • \d\{3\}与精确重复三次的数字匹配
  • -是字面的连字符

为避免转义,可使用\v:

1
/\v\d{3}-\d{3}-\d{4}

此模式还可用于捕获任何重复的数字,例如 IP 地址和邮政编码。

这涵盖了本章的搜索部分。 现在开始讲替换。

基本替换

Vim 的替代命令是一个有用的命令,用于快速查找和替换任何模式。 替换语法为:

1
:s/{old-pattern}/{new-pattern}/

让我们从一个基本用法开始。 如果您有以下文字:

1
vim is good

让我们用"awesome"代替"good",因为 Vim 很棒。 运行:s/good/awesome/.您应该看到:

1
vim is awesome

重复最后一次替换

您可以使用普通模式命令&或运行:s来重复最后一个替代命令。 如果您刚刚运行:s/good/awesome/,则运行&:s将会重复执行。

另外,在本章前面,我提到您可以使用//来重复先前的搜索模式。 此技巧可用于替代命令。 如果/good是最近被替换的单词,那么将第一个替换模式参数留为空白,例如在:s//awesome/中,则与运行:s/good/awesome/相同。

替换范围

就像许多 Ex 命令一样,您可以将范围参数传递给替换命令。 语法为:

1
:[range]s/old/new/

如果您有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要将第3行到第5行中的"let"替换为"const",您可以执行以下操作:

1
:3,5s/let/const/

下面是一些你可以使用的范围参数的变体:

  • :,3/let/const/ - 如果逗号前没有给出任何内容,则表示当前行。 从当前行替换到第 3 行。
  • :1,s/let/const/ - 如果逗号后没有给出任何内容,它也代表当前行。 从第 1 行替换到当前行。
  • :3s/let/const/ - 如果仅给出一个值作为范围(不带逗号),则仅在该行进行替换。

在 Vim 中,%通常表示整个文件。 如果运行:%s/let/const/,它将在所有行上进行替换。请记住这个范围参数语法,在后面章节中很多命令行命令都遵循这个语法。

模式匹配

接下来的几节将介绍基本的正则表达式。 丰富的模式知识对于掌握替换命令至关重要。

如果您具有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要在数字周围添加一对双引号:

1
:%s/\d/"\0"/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

让我们分解一下命令:

  • :%s 定位整个文件以执行替换。
  • \d 是 Vim 的预定义数字范围简写(类似使用[0-9])。
  • "\0" 双引号是双引号的字面值。 \0是一个特殊字符,代表"整个匹配的模式"。 此处匹配的模式是单个数字\d

另外,&也同样代表"整个匹配的模式",就像\0一样。 :s/\d/"&"/也可以。

让我们考虑另一个例子。 给出以下表达式,您需要将所有的"let"和变量名交换位置:

1
2
3
4
5
one let = "1";
two let = "2";
three let = "3";
four let = "4";
five let = "5";

为此,请运行:

1
:%s/\(\w\+\) \(\w\+\)/\2 \1/

上面的命令包含太多的反斜杠,很难阅读。 使用\v运算符更方便:

1
:%s/\v(\w+) (\w+)/\2 \1/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

太好了! 让我们分解该命令:

  • :%s 定位文件中的所有行以执行替换操作
  • (\w+) (\w+)对模式进行分组。\w是 Vim 预定义的单词字符范围简写([0-9A-Za-z_])之一。 包围\w()将匹配的单词字符进行分组。 请注意两个分组之间的空间。 (\w+) (\w+) 捕获两个分组。 在第一行上,第一组捕获"let",第二组捕获"one"。(英文版中,作者写成了:第一组捕获"one",第二组捕获"two",可能是作者不小心的错误)。
  • \2 \1 以相反的顺序返回捕获的组。 \2包含捕获的字符串"let",而\1包含字符串"one"。 使\2 \1返回字符串"let one"。

回想一下,\0代表整个匹配的模式。 您可以使用( )将匹配的字符串分成较小的组。 每个组都由\1, \2, \3等表示。

让我们再举一个例子来巩固这一匹配分组的概念。 如果您有以下数字:

1
2
3
123
456
789

要颠倒顺序,请运行:

1
:%s/\v(\d)(\d)(\d)/\3\2\1/

结果是:

1
2
3
321
654
987

每个(\d)都匹配一个数字并创建一个分组。 在第一行上,第一个(\d)的值为"1",第二个(\d)的值为"2",第三个(\d)的值为"3"。 它们存储在变量\1\2\3中。 在替换的后半部分,新模式\3\2\1在第一行上产生"321"值。

相反,如果您运行下面的命令:

1
:%s/\v(\d\d)(\d)/\2\1/

您将获得不同的结果:

1
2
3
312
645
978

这是因为您现在只有两个组。 被(\d\d)捕获的第一组存储在\1内,其值为"12"。 由(\d)捕获的第二组存储在\2内部,其值为"3"。 然后,\2\1返回"312"。

替换标志

如果您有以下句子:

1
chocolate pancake, strawberry pancake, blueberry pancake

要将所有 pancakes 替换为 donut,您不能只运行:

1
:s/pancake/donut

上面的命令将仅替换第一个匹配项,返回的结果是:

1
chocolate donut, strawberry pancake, blueberry pancake

有两种解决方法。 一,您可以再运行两次替代命令。 二,您可以向其传递全局(g)标志来替换一行中的所有匹配项。

让我们谈谈全局标志。 运行:

1
:s/pancake/donut/g

Vim 迅速将所有"pancake"替换为"donut"。 全局命令是替代命令接受的几个标志之一。 您在替代命令的末尾传递标志。 这是有用的标志的列表:

1
2
3
4
5
6
&    重用上一个替代命令中的标志。 必须作为第一个标志传递。
g 替换行中的所有匹配项。
c 要求替代确认。
e 防止替换失败时显示错误消息。
i 执行不区分大小写的替换
I 执行区分大小写的替换

我上面没有列出更多标志。 要了解所有标志,请查看:h s_flags

顺便说一句,重复替换命令(&:s)不保留标志。 运行&只会重复:s/pancake/donut/而没有g。 要使用所有标志快速重复最后一个替代命令,请运行:&&

更改定界符

如果您需要用长路径替换 URL:

1
https://mysite.com/a/b/c/d/e

要用单词"hello"代替它,请运行:

1
:s/https:\/\/mysite.com\/a\/b\/c\/d\/e/hello/

但是,很难说出哪些正斜杠(/)是替换模式的一部分,哪些是分隔符。 您可以使用任何单字节字符(除字母,数字或"|\之外的字符)来更改定界符。让我们将它们替换为+。上面的替换命令可以重写为 :

1
:s+https:\/\/mysite.com\/a\/b\/c\/d\/e+hello+

现在,更容易看到分隔符在哪里。

特殊替换

您还可以修改要替换的文本的大小写。 给出以下表达式,您的任务是将所有变量名比如 “one”, “two”, "three"等,改成大写:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

请运行:

1
%s/\v(\w+) (\w+)/\1 \U\2/

你会得到:

1
2
3
4
5
let ONE = "1";
let TWO = "2";
let THREE = "3";
let FOUR = "4";
let FIVE = "5";

这是该命令的细分:

  • (\w+) (\w+)捕获前两个匹配的分组,例如"let"和"one"。
  • \1返回第一个组的值"let"
  • \U\2大写(\U)第二组(\2)。

该命令的窍门是表达式\U\2\U将后面跟着的字符变为大写。

让我们再举一个例子。 假设您正在编写 Vim 书籍,并且需要将一行中每个单词的首字母大写。

1
vim is the greatest text editor in the whole galaxy

您可以运行:

1
:s/\<./\U&/g

结果:

1
Vim Is The Greatest Text Editor In The Whole Galaxy

细目如下:

  • :s 替换当前行
  • \<. 由两部分组成:\<匹配单词的开头,.匹配任何字符。 \<运算符使后面跟着的字符表示单词的第一个字符。 由于.是下一个字符,因此它将匹配任意单词的第一个字符。
  • \U& 将后续符号子序列&大写。 回想一下,&(或\0)代表整个匹配。 这里它匹配单词的第一个字符。
  • g全局标志。 没有它,此命令将仅替换第一个匹配项。 您需要替换此行上的每个匹配项。

要了解替换的特殊替换符号(如\u\U)的更多信息,请查看:h sub-replace-special

候选模式

有时您需要同时匹配多个模式。 如果您有以下问候:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

您仅需在包含单词"hello"或"hola"的行上用"friend"代替"vim"。回想一想本章前面的知识点,你可以使用| 来分隔可选的模式:

1
:%s/\v(hello|hola) vim)/\1 friend/g

结果:

1
2
3
4
hello friend
hola friend
salve vim
bonjour vim

这是细分:

  • %s 在文件的每一行上运行替代命令。
  • (hello|hola) 匹配*“hello"或"hola”,并将其视为一个组。
  • vim 是字面意思"vim"。
  • \1 是第一个匹配组,它是文本"hello"或"hola"。
  • friend 是字面的“朋友"。

指定替换模式的开始位置和结束位置

回想一下,您可以使用\zs\ze来指定一个匹配的开始位置和结束位置。这个技术在替换操作中同样有效,如果你有以下文本:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotcake

要想将"hotcake"中的"cake"替换为"dog",得到"hotdog":

1
:%s/hot\zscake/dog/g

结果是:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotdog

贪婪与非贪婪

您可以使用下面技巧,在某行中替换第n个匹配:

1
One Mississippi, two Mississippi, three Mississippi, four Mississippi, five Mississippi.

要想将第3个"Mississippi"替换为 “Arkansas”,运行:

1
:s/\v(.{-}\zsMississippi){3}/Arkansas/g

命令分解:

  • :s/ 替换命令。
  • \v 魔术关键字,使您不必转义特殊字符。
  • . 匹配任意单个字符。
  • {-} 表示使用非贪婪模式匹配前面的0个或多个字符。
  • \zsMississippi 使得从"Mississippi"开始捕获匹配。
  • (...){3} 查找第3个匹配

在本章前面的内容中,你已经看到过{3}这样的语法。在本例中,{3}将精确匹配第3个匹配。这里的新技巧是{-}。它表示进行非贪婪匹配。它会找到符合给定模式的最短的匹配。在本例中,(.{-}Mississippi)匹配以任意字符开始、数量最少的"Mississippi"。对比(.*Mississippi),后者会找到符合给定模式的最长匹配。

如果您使用(.{-}Mississippi),你会得到5个匹配:“One Mississippi”, “Two Mississippi”,等。如果您使用(.*Mississippi),您只会得到1个匹配:最后一个 “Mississippi”。*表示贪婪匹配,而{-}表示非贪婪匹配。要想了解更多,可以查看 :h /\{-:h non-greedy

让我们看一个简单的例子。如果您有以下字符串:

1
abc1de1

用贪婪模式匹配 “abc1de1” :

1
/a.*1

用非贪婪模式匹配 “abc1”:

1
/a.\{-}1

因此,如果您需要将最长的匹配转为大写(贪婪模式),运行:

1
:s/a.*1/\U&/g

会得到:

1
ABC1DE1

如果您需要将最短的匹配转为大写(非贪婪模式),运行:

1
:s/a.\{-}1/\U&/g

会得到:

1
ABC1de1

如果您是第一次接触贪婪模式与非贪婪模式这两个概念,可能会把你绕晕。围绕不同的组合去实验,知道您明白这两个概念。

跨多个文件替换

最后,让我们学习如何在多个文件中替换短语。对于本节,假设您有两个文件: food.txtanimal.txt.

food.txt内:

1
2
3
corn dog
hot dog
chili dog

animal.txt内:

1
2
3
large dog
medium dog
small dog

假设您的目录结构如下所示:

1
2
├ food.txt
├ animal.txt

首先,用:args同时捕获"food.txt"和"animal.txt"到参数列表中。回顾前面的章节,:args可用于创建文件名列表。在 Vim 中有几种方法可以做到这一点,其中一种方法是在Vim内部运行:

1
:args *.txt                  捕获当前位置的所有txt文件

测试一下,当您运行:args时,您应该会看到:

1
[food.txt] animal.txt

现在,所有的相关文件都已经存储在参数列表中,您可以用 :argdo 命令跨多文件替换,运行:

1
:argdo %s/dog/chicken/

这条命令对所有:args列表中的文件执行替换操作。最终,存储修改的文件:

1
:argdo update

:args:argdo 是两个有用的工具,用于跨多文件执行命令行命令。可以用其他命令结合尝试一下!

用宏跨多个文件替换

另外,您也可以用宏跨多个文件运行替代命令。执行:

1
2
3
4
5
6
:args *.txt
qq
:%s/dog/chicken/g
:wnext
q
99@q

以下是步骤的细分:

  • :args *.txt 会将相关文件列出到:args列表中。
  • qq 启动"q"寄存器中的宏。
  • :%s/dog/chicken/g在当前文件的所有行上用"chicken"替换"dog"。
  • :wnext 写入(保存)文件,然后转到args列表中的下一个文件。就像同时运行:w:next一样。
  • q 停止宏录制。
  • 99@q 执行宏九十九次。 Vim 遇到第一个错误后,它将停止执行宏,因此 Vim 实际上不会执行该宏九十九次。

以聪明的方式学习搜索和替换

良好的搜索能力是编辑的必要技能。掌握搜索功能使您可以利用正则表达式的灵活性来搜索文件中的任何模式。花些时间学习这些。要想掌握正则表达式,您必须在实践中去不断地使用它。我曾经读过一本关于正则表达式的书,却没有真正去做,后来我几乎忘了读的所有东西。主动编码是掌握任何技能的最佳方法。

一种提高模式匹配技能的好方法是,每当您需要搜索一个模式串时(例如"hello 123"),不要直接查询文字的字面值(/hello 123),去尝试使用模式串来搜索它(比如/\v(\l+) (\d+))。这些正则表达式概念中的许多不仅在使用 Vim 时,也适用于常规编程。

既然您已经了解了 Vim 中的高级搜索和替换,现在让我们学习功能最丰富的命令之一,即全局命令。

链接