这篇文章介绍 Vim 中的宏的概念,以及如何通过宏来实现一些高级编辑技巧。之后再介绍 Vim 中的模式匹配技巧。
宏
Vim 提供了不止一种方式用于重复之前所做的修改。之前已经学过了 .
命令,用它来重复小的修改确实有效,但是当我们想重复更大规模的改动时,Vim 的宏就派上用场了。
可以用宏把任意数目的按键操作录制到寄存器,用于之后的回放。学会 Vim 的宏只需要一分钟,但要穷其一生才能精通。
技巧 64:宏的读取与执行
宏允许我们把一段修改序列录制下来,用于之后的回放。
首先需要把命令序列录制成宏,q
键既是 录制
按钮,也是停止按钮。使用 q{register}
开始录制,并将命令序列保存到寄存器 register
中,直到再次按下 q
键时,录制停止。
接下来就可以通过执行宏来回放命令序列:可以使用 @{register}
命令执行指定寄存器的内容,也可以用 @@
来重复最近调用过的宏。
如下所示,如果想要对如下文本的每一行添加;号,可以按照如下方式录制宏:
1 | this is line 1 |
1 | qa |
之后,使用 j@a
来对第二行做同样修改,然后使用 @@
来对第三行做同样修改。
有几种技术可供我们在多次执行宏时使用。它们设置起来区别不大,重点在运行时遇到错误时,它们的处理方式有所不同。
- 以串行方式执行宏:在录制过程中,需要编写获取下一个操作对象的命令。另外,如果在执行过程中出现错误,则整个过程会提前终止
- 以并行方式执行宏:并行方式,对每一行都执行同样的宏。如果某一行出现错误,不影响其它行上宏的执行
无论采用串行还是并行技术,本质上 Vim 都是顺序执行宏。并行只不过是更健壮的一种执行方式,并不是说 Vim 会真正的并发执行多处修改。
技巧 65:规范光标位置、直达目标以及中止宏
当执行一个宏时,Vim 会机械地重复这个打包在一起的按键操作序列。所以使用宏时的一条黄金法则是:在录制一个宏时,需要确保每条命令都可被重复执行。
- 规范光标的位置:在录制宏时,一定要问自己几个问题:我在哪里?我从哪里来?我要去哪里?在做任何事之前,要确保你的光标位置已经就位
- 用可重复的动作命令直达目标:面向单词的动作命令,与面向字符的动作命令相比,更具灵活性。推荐使用查找命令或用文本对象定位。
- 当动作命令失败时,宏将中止执行:如果宏执行动作命令失败了,Vim 将中止执行宏的其余命令。这是一项功能,而不是漏洞
技巧 66:加次数回放宏
对于重复次数不多的工作,使用点范式是一种高效的编辑策略,但是它不能指定执行的次数。为了克服该限制,可以录制一个廉价的、一次性的宏,然后在加次数进行回放。
例如,对于如下文本:
1 | 1+2+3 |
如果想在每个“+”号两边增加空格,可以使用如下命令录制宏:
f+
:跳转到+
号s + <ESC>
:在+
号两边增加空格qq;.q
:录制重复修改命令宏5@q
:重复修改
其实最后这个重复执行宏的次数可以任意大,因为当执行结束时,查找命令 f+
将中止,此时宏的执行也将中止,最终总能得到正确的结果。
技巧 67:在连续的文本上重复修改
对于多行范围的重复性劳动,可以先录制一个宏,然后再在每一行上回放,这将会极大减轻我们的工作量。该功能可用两种执行宏的方式实现:串行或者并行。
对于对下文本:
1 | 1. one |
如果想变成如下格式的文本:
1 | 1) one |
可以使用如下命令录制宏:
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
:开始录制宏,保存内容到寄存器 qggOSTART<ESC>
:在文件首添加 STARTGoEND<ESC>
:在文件尾添加 END
录制完宏,不要进行 write:其原因是如果保存之后,再对所有文件执行该宏时,第一个文件就修改了两次。因此我们使用:edit!
命令放弃对当前文件的修改,但是宏已经录制完成了。
:argdo
命令允许我们对参数列表内所有缓冲区执行一条 Ex
命令。因此可以使用 :argdo normal @
命令对所有文件执行该宏。执行之前一定要先设置 :hidden
选项。
如果想以串行方式执行宏,可以在操作完缓冲区后,使用 :next
命令跳转到下一个文件。然后可以通过 @q
的方式执行该宏,前面的次数可以任意大。因为达到最后一个缓冲区后,:next
命令将执行失败,这个修改也就结束了。
在修改所有文件后,可以使用 :wall
命令保存所有文件。对于文件级别的修改,使用串行方式执行,可能更容易发现错误(执行失败,就直接停止在当前文件中)。
技巧 70:用迭代求值的方式给列表编号
如果想要为一些连续的行编号,例如对于如下行:
1 | one |
最终,如果想做成如下样子:
1 | 1) one |
如果想要做到这一点,可以使用表达式寄存器并结合 Vim 脚本。之前已经讲过,表达式寄存器可以进行简单的运算,并将结果插入至文档。
:let i=1
:创建变量 i,并赋值为 1qq
:开始录制宏I<Ctrl-r>=i <ESC>
:访问表达式寄存器:let i+=1
:自增变量iq
:结束录制宏
之后,便可以使用并行方式,在余下的文本行上回放这个宏:
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 | body { color: #3c3c3c; } |
如果采用默认的 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 | I love Paris in the |
可以使用如下正则表达式:
1 | /\v<(\w+)\_s+\1> |
- 任何圆括号内部的匹配文本都会被自动保存到一个临时的仓库。我们可以用
\1
引用这段被捕获的文本 - 如果模式中不止一组圆括号,则可以使用
\1
,\2
直到\9
,引用每对()
所捕获的子匹配 - 另外,不管是否使用了圆括号,都可以使用
\0
来引用整个匹配 - 另外该正则表达式使用
<>
来匹配单词的边界 - 最后,元字符
\_s
会匹配空白符或换行符
有时,我们只想使用圆括号的分组功能,并不关心所捕获的子匹配,此时可以在圆括号前面加上 %
,指示 Vim 不要将括号内的内容赋给寄存器 \1
。
技巧 76:界定单词的边界
有时候,我们需要匹配完整的单词,而不是其他词的组成部分,此时可以使用单词界定符。在 very magic
搜索模式下,用 <
与 >
符号表示单词界定符。这也是所谓的零宽度元字符,它们本省不匹配任何字符,仅表示单词与围绕此单词的空白字符(或标点符号)之间的边界。
在 very magic
搜索模式下,<
>
字符被直接解析为单词定界符。而在 magic
、nomagic
、very 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
参数指定哪些字符需要被 \
转义