声明

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

在编辑文件的时候,你会发现有时候你在反复地做一些相同的动作。如果你仅做一次,并在需要的时候调用这些动作岂不是会更好吗。通过 Vim 的宏命令,你可以将一些动作记录到 Vim 寄存器。

在本章中,你将会学习到如何通过宏命令自动完成一些普通的任务(另外,看你的文件在自动编辑是一件很酷的事情)。

基本宏命令

宏命令的基本语法如下:

1
2
qa                     开始记录动作到寄存器 a
q (while recording) 停止记录

你可以使用小写字母 (a-z)去存储宏命令。并通过如下的命令去调用:

1
2
@a    Execute macro from register a
@@ Execute the last executed macros

假设你有如下的文本,你打算将每一行中的所有字母都变为大写。

1
2
3
4
5
hello
vim
macros
are
awesome

将你的光标移动到 “hello” 栏的行首,并执行:

1
qa0gU$jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • gU$ 将从光标到行尾的字母变为大写。
  • j 移动到下一行。
  • q 停止记录。

调用 @a 去执行该宏命令。就像其他的宏命令一样,你也可以为该命令加一个计数。例如,你可以通过 3@a 去执行 a 命令3次。你也可以执行 3@@ 去执行上一次执行过的宏命令3次。

安全保护

在执行遇到错误的时候,宏命令会自动停止。假如你有如下文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

你想将每一行的第一个词变为大写,你可以使用如下的宏命令:

1
qa0W~jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 将光标选中的单词变为大写。
  • j 移动到下一行。
  • q 停止记录。

我喜欢对宏命令进行超过所需次数的调用,所以我通常使用 99@a 命令去执行该宏命令99次。使用该命令,Vim并不会真正执行这个宏99次,当 Vim 到达最后一行执行j时,它会发现无法再向下了,然后会抛出一个错误,并终止宏命令的执行。

实际上,遇到错误自动停止运行是一个很好的特性。否则,Vim 会继续执行该命令99次,尽管它已经执行到最后一行了。

命令行执行宏

在正常模式执行 @a 并不是宏命令调用的唯一方式。你也可以在命令行执行 :normal @a:normal 会将任何用户添加的参数作为命令去执行。例如添加 @a,和在 normal mode 执行 @a 的效果是一样的。

:normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a

在多个文件中执行宏命令

假如你有多个 .txt 文件,每一个文件包含不同的内容。并且你只想将包含有 “donut” 单词的行的第一个单词变为大写。假设,您的寄存器a中存储的内容是0W~j(就是前面例子中用到的宏命令),那么,您该如何快速完成这个操作呢?

第一个文件:

1
2
3
4
## savory.txt
a. cheddar jalapeno donut
b. mac n cheese donut
c. fried dumpling

第二个文件:

1
2
3
4
## sweet.txt
a. chocolate donut
b. chocolate pancake
c. powdered sugar donut

第三个文件:

1
2
3
## plain.txt
a. wheat bread
b. plain donut

你可以这么做:

  • :args *.txt 查找当前目录下的所有 .txt 文件。
  • :argdo g/donut/normal @a:args 中包含的每一个文件里执行一个全局命令 g/donut/normal @a
  • :argdo update:args 中包含的每一个文件里执行 update 命令,保存修改后的内容。

也许你对全局命令 :g/donut/normal @a 不是很了解,该命令会执行 /donut/搜索命令,然后在所有匹配的行中执行normal @a 命令。我会在后面的章节中介绍全局命令。

递归执行宏命令

你可以递归地执行宏命令,通过在记录宏命令时调用相同的宏寄存器来实现。假如你有如下文本,你希望改变第一个单词的大小写:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

如下命令会递归地执行:

1
qaqqa0W~j@aq

上面命令的分解如下:

  • qaq 记录一个空白的宏命令到 “a” 。把宏命令记录在一个空白的命令中是必须的,因为你不会想将该命令包含有任何其他的东西。
  • qa 开始录入宏命令到寄存器 “a”。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 改变光标选中的单词的大小写。
  • j 移动到下一行。
  • @a 执行宏命令 “a”。当你记录该宏命令时,@a 应该是空白的,因为你刚刚调用了 qaq
  • q 停止记录。

现在,让我们调用 @a 来查看 Vim 如何递归的调用该宏命令。

宏命令是如何知道何时停止呢?当宏执行到最后一行并尝试 j 命令时,发现已经没有下一行了,就会停止执行。

增添一个已知宏

如果你想在一个已经录制好的宏定义中添加更多的操作,与其重新录入它,不如选择修改它。在寄存器一章中,你学习了如何使用一个已知寄存器的大写字母来想该寄存器中添加内容。同样的,为了在寄存器"a"中添加更多的操作,你也可以使用大写字母"A"。

假设寄存器a中已经存储了这个宏命令:qa0W~q(该宏命令将某行的第二个词组的头一个字母执行改变大小写操作),假设你想在这个基础上添加一些操作命令序列,使得每一行末尾添加一个句点,运行:

1
qAA.<esc>q

分解如下:

  • qA 开始在寄存器 “A” 中记录宏命令。
  • A.<esc> 在行的末尾加上一个句点(这里的A是进入插入模式,不要和宏A搞混淆),然后退出插入模式。
  • q 停止记录宏命令。

现在,当你执行@a时,它不仅将第二个词组的首字母转变大小写,同时还在行尾添加一个句点。

修改一个已知宏

如果想在一个宏的中间添加新的操作该怎么办呢?

假设您在寄存器a中已经存有一个宏命令0W~A.<Esc>,即改变首字母大小写,并在行尾添加句号。如果您想在改变首字母大小写和行尾添加句号之间,在单词"dount"前面加入"deep fried"。(因为唯一比甜甜圈好的东西就是炸甜甜圈)。

我会重新使用上一节使用过的文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

首先,让我们通过 :put a 调用一个已经录制好的宏命令(假设你上一节中保存在寄存器a中的宏命令还在):

1
0W~A.^[

^[ 是什么意思呢?不记得了吗,你之前执行过 0W~A.<esc>^[ 是 Vim 的 内部指令,表示 <esc>。通过这些指定的特殊键值组合,Vim 知道这些是内部代码的一些替代。一些常见的内部指令具有类似的替代,例如 <esc><backspace><enter>。还有一些其他的键值组合,但这不是本章的内容。

回到宏命令,在改变大小写之后的键后面(~),让我们添加($)来移动光标到行末,回退一个单词(b),进入插入模式(i),输入"deep fried " (别忽略"fried "后面的这个空格),之后退出插入模式(<esc>)。

完整的命令如下:

1
0W~$bideep fried <esc>A.^[

这里有一个问题,Vim 不能理解 <esc>。您不能依葫芦画瓢输入"",所以你需要将<Esc>写成内部代码的形式。在插入模式,在按下<esc>后按下 Ctrl-v,Vim 会打印 ^[Ctrl-v 是一个插入模式的操作符,可以逐字地插入一个非数字字符。你的宏命令应该如下:

1
0W~$bideep fried ^[A.^[

为了在寄存器“a”中添加修改后的指令,你可以通过在一个已知命名寄存器中添加一个新条目的方式来实现。在一行的行首,执行 "ay$,使用寄存器 "a"来存储复制的文本。

现在,但你执行 @a 时,你的宏命令会自动改变第一个单词的大小写,在"donut"前面添加"deep fried ",之后在行末添加“.”。

另一个修改宏命令的方式是通过命令行表达式。执行 :let @a=",之后执行 Ctrl-r Ctrl-r a,这会将寄存器“a”的命令逐字打印出来。最后,别忘记在闭合的引号(")。如果你希望在编辑命令行表达式时插入内部码来使用特定的字符,你可以使用 Ctrl-v

拷贝宏

你可以很轻松的将一个寄存器的内容拷贝到另一个寄存器。例如,你可以使用 :let @z = @a 将寄存器"a" 中的命令拷贝到寄存器"z"。 @a 表示寄存器“a”中存储的内容,你现在执行 @z,将会执行和 @a 一样的指令。

我发现对常用的宏命令创建冗余是很有用的。在我的工作流程中,我通常在前7个字母(a-g)上创建宏命令,并且我经常不加思索地把它们替换了。因此,如果我将很有用的宏命令移动到了字母表的末尾,就不用担心我在无意间把他们替换了。

串行宏和并行宏

Vim 可以连续和同时运行宏命令,假设你有如下的文本:

1
2
3
4
5
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

假如你希望把所有的 “FUNC” 字符变为小写,那么宏命令为如下:

1
qa0f{gui{jq

分解如下:

  • qa 开始记录宏命令到 “a” 寄存器。
  • 0移动到第一行。
  • f{ 查找第一个 “{” 字符。
  • gui{ 把括号内的文本(i{)变为小写(gu)。
  • j 移动到下一行。
  • q 停止记录宏命令。

现在,执行 99@a 在剩余的行修改。然而,假如在你的文本里有如下 import 语句会怎么样呢?

1
2
3
4
5
6
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import foo from "bar";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

执行 99@a,会只在前三行执行。而最后两行不会被执行,因为在执行第四行(包含“foo”)时f{命令会遇到错误而停止,当宏串行执行时就会发生这样的情况。当然,你仍然可以移动到包含(“FUNC4”)的一行,并重新调用该命令。但是假如你希望仅调用一次命令就完成所有操作呢?

你可以并行地执行宏命令。

如本章前面所说,可以使用 :normal 去执行宏命令,(例如: :3,5 normal @a 会在 3-5行执行 a 寄存器中的宏命令)。如果执行 :1,$ normal @a,会在所有除了包含有 “foo” 的行执行,而且它不会出错。

尽管本质上来说,Vim 并不是在并行地执行宏命令,但表面上看,它是并行运行的。 Vim 会独立地在从第一行开始(1,$)每一行执行 @a 。由于 Vim 独立地在每一行执行命令,每一行都不会知道有一行(包含“foo”)会遇到执行错误。

聪明地学习宏命令

你在编辑器里做的很多事都是重复的。为了更好地编辑文件,请乐于发现这些重复性的行为。执行宏命令或者点命令,而不是做相同的动作两次。几乎所有你在 Vim 所作的事情都可以变为宏命令。

刚开始的时候,我发现宏命令时很棘手的,但是请不要放弃。有了足够的练习,你可以找到这种文本自动编辑的快乐。

使用某种助记符去帮助你记住宏命令是很有帮助的。如果你有一个创建函数(function)的宏命令,你可以使用 “f” 寄存器去录制它(qf)。如果你有一个宏命令去操作数字,那么使用寄存器 “n” 去记住它是很好的(qn)。用你想执行的操作时想起的第一个字符给你的宏命令命名。另外,我发现 “q” 是一个很好的宏命令默认寄存器,因为执行 qq 去调用宏命令是很快速而简单的。最后,我喜欢按照字母表的顺序去添加我的宏命令,例如从 qaqb 再到 qc

去寻找最适合你的方法吧。

链接