0%

Vim 实用技巧(06):宏 & 模式匹配

这篇文章介绍 Vim 中的宏的概念,以及如何通过宏来实现一些高级编辑技巧。之后再介绍 Vim 中的模式匹配技巧。

Vim 提供了不止一种方式用于重复之前所做的修改。之前已经学过了 . 命令,用它来重复小的修改确实有效,但是当我们想重复更大规模的改动时,Vim 的宏就派上用场了。

可以用宏把任意数目的按键操作录制到寄存器,用于之后的回放。学会 Vim 的宏只需要一分钟,但要穷其一生才能精通。

技巧 64:宏的读取与执行

宏允许我们把一段修改序列录制下来,用于之后的回放。

首先需要把命令序列录制成宏,q 键既是 录制 按钮,也是停止按钮。使用 q{register} 开始录制,并将命令序列保存到寄存器 register 中,直到再次按下 q 键时,录制停止。

接下来就可以通过执行宏来回放命令序列:可以使用 @{register} 命令执行指定寄存器的内容,也可以用 @@ 来重复最近调用过的宏。

如下所示,如果想要对如下文本的每一行添加;号,可以按照如下方式录制宏:

1
2
3
this is line 1
this is line 2
this is line 3
1
2
3
4
qa
A;
ESC
q

之后,使用 j@a 来对第二行做同样修改,然后使用 @@ 来对第三行做同样修改。

有几种技术可供我们在多次执行宏时使用。它们设置起来区别不大,重点在运行时遇到错误时,它们的处理方式有所不同。

  • 以串行方式执行宏:在录制过程中,需要编写获取下一个操作对象的命令。另外,如果在执行过程中出现错误,则整个过程会提前终止
  • 以并行方式执行宏:并行方式,对每一行都执行同样的宏。如果某一行出现错误,不影响其它行上宏的执行

无论采用串行还是并行技术,本质上 Vim 都是顺序执行宏。并行只不过是更健壮的一种执行方式,并不是说 Vim 会真正的并发执行多处修改。

技巧 65:规范光标位置、直达目标以及中止宏

当执行一个宏时,Vim 会机械地重复这个打包在一起的按键操作序列。所以使用宏时的一条黄金法则是:在录制一个宏时,需要确保每条命令都可被重复执行。

  • 规范光标的位置:在录制宏时,一定要问自己几个问题:我在哪里?我从哪里来?我要去哪里?在做任何事之前,要确保你的光标位置已经就位
  • 用可重复的动作命令直达目标:面向单词的动作命令,与面向字符的动作命令相比,更具灵活性。推荐使用查找命令或用文本对象定位。
  • 当动作命令失败时,宏将中止执行:如果宏执行动作命令失败了,Vim 将中止执行宏的其余命令。这是一项功能,而不是漏洞

技巧 66:加次数回放宏

对于重复次数不多的工作,使用点范式是一种高效的编辑策略,但是它不能指定执行的次数。为了克服该限制,可以录制一个廉价的、一次性的宏,然后在加次数进行回放。

例如,对于如下文本:

1
1+2+3

如果想在每个“+”号两边增加空格,可以使用如下命令录制宏:

  • f+:跳转到 +
  • s + <ESC>:在 + 号两边增加空格
  • qq;.q:录制重复修改命令宏
  • 5@q:重复修改

其实最后这个重复执行宏的次数可以任意大,因为当执行结束时,查找命令 f+ 将中止,此时宏的执行也将中止,最终总能得到正确的结果。

技巧 67:在连续的文本上重复修改

对于多行范围的重复性劳动,可以先录制一个宏,然后再在每一行上回放,这将会极大减轻我们的工作量。该功能可用两种执行宏的方式实现:串行或者并行。

对于对下文本:

1
2
3
4
1. one
2. two
3. three
4. four

如果想变成如下格式的文本:

1
2
3
4
1) one
2) two
3) three
4) four

可以使用如下命令录制宏:

  • qq:开始录制
  • 0f.:移动到 .
  • r):替换
  • W~:移动到单词首部,进行大小写转换
  • j:跳转到下一行
  • q:结束录制

以串行方式执行宏:之后便可以通过 10@q 的方式在每行上执行该宏,由于宏命令的最后都是到下一行,所以该宏可以在每行上起作用。当遇到文件尾时,j 命令失败,此时宏执行结束,任务完成。

但是这种方式有时候也会有问题:如果中途执行某行失败时,整个宏的执行将终止,安全捕获机制截住了我们。但是这意味着其它行没有得到修改。此时需要对之后的行再次调用该宏进行执行。

以并行方式执行宏:可以通过 :normal command 的方式,对所有行执行宏命令,这就是并行执行宏的方式。因此可以通过如下命令录制宏:

  • qq0r)ESCW~q:录制修改宏
  • jVG:选中剩余的所有行
  • :normal @q:对所有行执行该宏。

以这种方式执行时,即使在某行出现失败,也不影响其它行的修改。

到底是使用串行还是并行,需要看情况分析。以并行的方式在多处执行宏时更为健壮。但如果是想利用执行出错时的警告,以串行方式更容易定位问题所在。

技巧 68:给宏追加命令

在宏的录制过程中,如果漏掉了某个重要的步骤,此时没有必要从头开始重新录制,而是可以在现有宏的结尾附加额外的命令。

在输入 q{register} 时,如果 register 为小写名称时,Vim 将录制按键操作并将其保存到寄存器中,此时会覆盖寄存器原有的内容。如果 register 为大小名称,Vim 则将按键操作附加到相应小写寄存器内容之后。

因此,这条技巧只适用于在结尾添加命令,如果想要在开头或中间添加内容,它就无能为力了。

技巧 69:在一组文件中执行宏

Vim 支持跨文件回放宏。例如,如果想在每个文件前的行首加上 START,在行尾加上 END。在用 Vim 打开所有文件后,在第一个文件中录制如下宏:

  • qq:开始录制宏,保存内容到寄存器 q
  • ggOSTART<ESC>:在文件首添加 START
  • GoEND<ESC>:在文件尾添加 END

录制完宏,不要进行 write:其原因是如果保存之后,再对所有文件执行该宏时,第一个文件就修改了两次。因此我们使用:edit! 命令放弃对当前文件的修改,但是宏已经录制完成了。

:argdo 命令允许我们对参数列表内所有缓冲区执行一条 Ex 命令。因此可以使用 :argdo normal @ 命令对所有文件执行该宏。执行之前一定要先设置 :hidden 选项。

如果想以串行方式执行宏,可以在操作完缓冲区后,使用 :next 命令跳转到下一个文件。然后可以通过 @q 的方式执行该宏,前面的次数可以任意大。因为达到最后一个缓冲区后,:next 命令将执行失败,这个修改也就结束了。

在修改所有文件后,可以使用 :wall 命令保存所有文件。对于文件级别的修改,使用串行方式执行,可能更容易发现错误(执行失败,就直接停止在当前文件中)。

技巧 70:用迭代求值的方式给列表编号

如果想要为一些连续的行编号,例如对于如下行:

1
2
3
one
two
three

最终,如果想做成如下样子:

1
2
3
1) one
2) two
3) three

如果想要做到这一点,可以使用表达式寄存器并结合 Vim 脚本。之前已经讲过,表达式寄存器可以进行简单的运算,并将结果插入至文档。

  • :let i=1:创建变量 i,并赋值为 1
  • qq:开始录制宏
  • I<Ctrl-r>=i <ESC>:访问表达式寄存器
  • :let i+=1:自增变量i
  • q:结束录制宏

之后,便可以使用并行方式,在余下的文本行上回放这个宏:

  • jVG:选中剩余的行
  • :normal @q:在每行上回放宏

技巧 71:编辑宏的内容

技巧 68 中看到过,在宏的结尾添加命令非常容易,但是如果想修改宏的内容就需要使用其他方式了。我们可以像编辑普通文本一样编辑宏的内容。

用于录制宏的寄存器,与用作复制、粘贴操作的寄存器是一样的。因此,如果想要修改寄存器 a 中的内容话,只需要将寄存器粘贴到文档中,然后按普通编辑文件方式即可编辑宏的内容。

使用 :put a 命令将寄存器 a 中的内容粘贴到当前光标所在行的下一行("ap 命令可不是,因为此时寄存器中的内容是面向字符的)。

修改宏内容后,需要将宏从文档复制回寄存器:使用 "ay$,即将当前宏的内容重新复制会寄存器a。

将宏粘贴到文档中进行编辑,然后复制回寄存器执行,这样的确很方便。但是由于宏中键盘编码等原因,导致编辑宏的内容需要非常小心。由于寄存器只不过是保存文本串的容器,因此可以通过编写 Vim 脚本的方式操作它们。例如通过 substitue() 函数替换寄存器中内容(参考 :h function-list)。

模式匹配

Vim 的正则表达式引擎可能与你惯用的其它引擎有所不同。接下来将介绍 Vim 中的模式匹配技巧。

技巧 72:调整查找模式的大小写敏感性

可以全局性地调整 Vim 查找功能的大小写敏感性,也可以在每次查找时进行局部调整。

  • 全局设置大小写敏感性:启用 ignorecase 设置,Vim 的查找模式将不区分大小写。需要注意,这不仅影响 Vim 的查找,还会影响 Vim 的自动补全行为
  • 每次查找时设置大小写敏感性:使用元字符 \c\C,可以覆盖 Vim 缺省的大小写敏感性设置。
    • 小写字母 \c 会让查找模式忽略大小写
    • 而大写字母 \C 则会强制区分大小写

Vim 的 smartcase 选项,可以最大限度地推测我们是想用大写还是小写。启用该选项后,只要我们在查找模式中输入了大写字母,则 ignorecase 设置不再生效。但如果使用了元字符 \c\C,还是其强制效果生效。

技巧 73:按正则表达式查找时,使用 \v 模式开关

与 Perl 相比,Vim 的正则表达式的语法风格更接近 POSIX。但是通过使用 very magic 开关,就可以让 Vim 采用我们更为熟悉的正则表达式语法了。

例如,如果想要在如下文件中匹配出 1个 # 字符及紧随其后的3个或6个十六进制字符

1
2
3
body   { color: #3c3c3c; }
a   { color: #0000EE; }
strong { color: #000; }

如果采用默认的 magic 搜索模式,需要输入如下正则表达式:

1
/#\([0-9a-fA-F]\{6}\|[0-9a-fA-F]\{3}\)

在这里,方括号默认具有特殊含义,不需要转义。圆括号会按原义匹配字符,因此需要转义。花括号也需要转义,只不过只需要为开括号转义,而对应的闭括号则不用。

可以使用 \v 模式开关来统一所有的特殊符号规则。\v 元字符会激活 very magic 搜索模式,即假定所有除大小写字母、下划线和数字0-9外的所有字符都具有特殊含义。

因此,通过如下表达式,也可以达到目的:

1
/\v#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})

另外,我们可以使用 \x 来优化字符集 [0-9a-fA-F]

1
/\v#(\x{6}|\x{3})

技巧 74:按原义查找文本时,使用\V原义开关。

如果想按原义查找文本,可以使用 very nomagic 原义开关。这样就可以消除附加在 .* 以及 ? 等绝大多数字符的特殊含义。使用原义开关 \V ,激活 very nomagic 搜索模式。\V 原义开关使得其后的模式中只有反斜杠才有特殊的意义。

very nomagic 搜索模式下,创建正则表达式也是可行的,只不过需要对每个符号进行转义。因此作为通用规则:**如果你想按正则表达式查找,就用模式开关 \v,如果你想按原义查找文本,就用原义开关 \V**。

技巧 75:使用圆括号捕获子匹配

当我们在指定一个模式时,可以捕获其子匹配,并在其它地方引用它们。例如如果想要在如下文本中找出连续两次重复出现的单词:

1
2
I love Paris in the
the springtime.

可以使用如下正则表达式:

1
/\v<(\w+)\_s+\1>
  • 任何圆括号内部的匹配文本都会被自动保存到一个临时的仓库。我们可以用 \1 引用这段被捕获的文本
  • 如果模式中不止一组圆括号,则可以使用 \1\2 直到 \9,引用每对 () 所捕获的子匹配
  • 另外,不管是否使用了圆括号,都可以使用 \0 来引用整个匹配
  • 另外该正则表达式使用 <> 来匹配单词的边界
  • 最后,元字符 \_s 会匹配空白符或换行符

有时,我们只想使用圆括号的分组功能,并不关心所捕获的子匹配,此时可以在圆括号前面加上 %,指示 Vim 不要将括号内的内容赋给寄存器 \1

技巧 76:界定单词的边界

有时候,我们需要匹配完整的单词,而不是其他词的组成部分,此时可以使用单词界定符。在 very magic 搜索模式下,用 <> 符号表示单词界定符。这也是所谓的零宽度元字符,它们本省不匹配任何字符,仅表示单词与围绕此单词的空白字符(或标点符号)之间的边界。

very magic 搜索模式下,< >字符被直接解析为单词定界符。而在 magicnomagicvery nomagic 搜索模式下,必须要将它们转义。

其实使用 *# 来正向或方向查找光标所在的单词时,就使用了单词定界符。另外,g*g# 这两个变体执行类似的查找,但是不会使用单词定界符。

技巧 77:界定匹配的边界

有时候,可能想指定一个范围较广的模式,但只对匹配结果的一部分感兴趣。Vim 中的元字符 \zs\ze 可以帮助我们处理这种情况。

首先需要明确模式和匹配的区别。模式是指在查找域中输入的正则表达式,而匹配是指在文档中被高亮显示的文本。一个匹配的边界通常对应于一个模式的起始和结尾。但是我们可以使用元字符 \zs\ze 对匹配进行裁剪,使其成为完整模式的一个子集。

元字符 \zs 标志着一个匹配的起始,而元字符 \ze 则用来界定匹配的结束。\zs\ze 也都属于零宽度元字符。

例如,想高亮匹配出所有双引号内所包含的文本,如果使用如下搜索模式,则会把双引号也高亮匹配出来。

1
/\v"[^"]+"

通过如下模式,可以限定匹配范围:

1
/\v"\zs[^"]+\ze"

此时,则只匹配出双引号内的文本,但是双引号仍然是模式的一部分。

技巧 78:转义问题字符

very nomagic 模式下,几乎所有字符的特殊含义都被屏蔽掉了。但是,还有些字符其特殊含义无法被屏蔽:

  • 正向查找时要转义 / 字符:在正向查找时,无论执行的是 very magic 查找,还是 very nomagic 查找,都必须转义符号 /。因为该符号的含义默认为查找域的结束符
  • 反向查找时要转义 ? 字符:当执行反向查找时,? 会被当做查找域的结束符。因此如果查找内容中包含 ? 字符,则需要转义该字符
  • 每次都要转义符号 \: \ 字符代表转义字符,所以如果想要查找 \ 本身,则需要转义 \ 字符,即用 \\ 代表 \ 字符

用编程的方式转义字符:用手工方式转义字符比较费力,Vim 脚本提供了一个库函数 escape({string}, {chars}) 来完成这项任务。chars 参数指定哪些字符需要被 \ 转义