/images/vim-keyboard-cover-macbook.thumbnail.jpg

Vim 编辑器的优势和劣势,我已经了解了不少。作为开发者,使用 Vim 要比其它编辑器要快一些,看起来确是如此。然而,我正在使用 Vim 做一些基础编辑,却时常感到效率慢了 10 倍不止。

当谈及编辑速度时(你应该更多地关注于此),我们把重点放在两个方面:

  1. 交替地使用左右手,是操作键盘 最快 的方式;

  2. 尽可能不碰鼠标,是追求极致速度的唯二法门。移动手腕,抓取鼠标,移动鼠标,手指返回键盘……需要花费极长时间(更别说,通常你不得不低头盯着键盘,确保手指放到了正确的位置)。,

我有以下两个例子,来说明使用 Vim 为何没有体验到效率提升。

复制/剪切 和 粘贴 。我经常得做这事。如果是现代的其它编辑器,我用左手按住 Shift 键,右手移动光标选择文字。然后, Ctrl + C 拷贝,移动光标, Ctrl + V 粘贴。

而使用 Vim 就很糟糕:

  • yy 拷贝一行(通常我才不需要一整行啊!)

  • [number xx]yy 拷贝 xx 行到缓冲区。但是,你不知道是否选对了想要的行,以至于我经常不得不 [number xx]dd ,然后再按 u 来撤销!

另一个例子? 搜索和替换

  • 使用 PSPadCtrl + f 然后键入想要搜索的,再按 Enter

  • 使用 Vim: / ,然后键入搜索内容,如有特殊字符则在前面加个 \ ,然后按 Enter 键。

Vim 里的其它东西,看起来亦是如此:我不知道如何正确地使用它们。

提示: 我已经读过 Vim 备忘 小抄 :)

我的问题是:

你是如何使用 Vim 编辑器的,以使得它比其它编辑器更有效率?


关于 Vim 编辑器,你的问题是没有领悟 Vi 的精髓。

你提到使用 yy 剪切,然后抱怨几乎从没想要剪切整个行。事实是,程序员编辑代码时,经常需要对整个行,多个行以及代码块进行操作。然而, yy 只是将文本复制到匿名缓冲区( vi 称之为寄存器)的其中一种方法。

vi 之禅在于,你是在说一种语言。此处的第一个 y 是个动词,而 yy 则与 y_ 是同一种意思。两个 y 使得它更容易输入,因为这是一个常用操作。

再用 dd P 来解释一遍(删除当前行,再把它粘贴回原处;同时,在匿名寄存器内留下一份拷贝)。动词 yd 对其“主体”执行动作。因此 , yW 是从此处(光标处)拷贝到当前/下一个(big)单词末尾,而 y'a 则是从此处拷贝到 a 标记所在的行。

如果你仅了解基本的上下左右光标移动,则 vi 不会比 notepad 更有效率(好吧,你仍然拥有语法高亮以及打开超过 45KB 文件的能力。但是,意义何在呢?)。

vi 拥有 26 个“标记”和 26 个“寄存器”。标记使用 m 命令,放置到任何光标位置,由单个小写字符指定。因此 ma 将当前位置设置为 a ,而 mz 设置为 z 标记。你可以使用 ' 单引号跳转到标记的行。因此, 'a 跳转到 a 标记的那一行的开头。使用 ` 反引号可跳转到任何标记的精确位置。因此, `z 将跳转到 z 标记的精确位置。

因为这些都是“动作”,它们也可作用于其它指令的“主体”。

所以,剪切任意文本的一种方式是:首先进行标记(我通常使用 "a" 作为第一个标记,"z" 作为下一个标记,"b" 是下一个,"e" 下一个;在使用 vi 的 15 年中,我从不记得使用过 4 个以上的标记;在不影响交互场景的前提下,决定在宏中使用多少个标记和寄存器,并养成自己的习惯)。然后,我们就可以使用 d`a 来剪切或 y`a 来拷贝。因此整个流程仅需要 5 次键击(如果处在“插入”模式则是 6 次,需要按 Esc 键进入命令模式)。一旦完成剪切或拷贝,只需要一次按键 p 就可以粘贴了。

我说过,这是剪切或复制文本的一种方式。然而,它只是众多方式中的一种。通常,我们可以更简练地描述文本范围,而无需移动光标并放置标记。比如,如果在段落中,可以使用 {}} 分别跳转到其开头或结尾。想要移动文本段落,可使用 { d} (三次键击)来剪切。如果碰巧在段落的首行或末行,可以直接分别使用 d}d{

此处的“段落”,与我们通用的概念意思一致。因此,它适用于代码和散文。

通常,我们知道有某些模式(正则表达式),可标记感兴趣文本的一端或另一端。在 vi 里,向前或向后搜索被视为动作。因此,在“指令”中,亦可作用于“主体”。我们可以使用 d/foo 从当前行剪切,直到包含 "foo" 的那一行,用 y?bar 来从当前行向前拷贝到包含 "bar" 最近的那一行。如果不想要整个行,还可以使用搜索动作,像之前那样,放置标记然后使用 `x 标记。

除了“动词”和“主体”之外, vi 还有语法含义上的“对象”。目前为止,我只讲了匿名寄存器的使用。然而,你还可以在“对象”前添加 " 双引号,来使用 26 个“命名”寄存器中的任何一个。因此,使用 "add 意为剪切当前行存入 "a" 寄存器,而 "by/foo" 则是从此处拷贝到包含 "foo" 的那一行,并存入 "b" 寄存器。要从某寄存器粘贴,则在粘贴前按同样的按键序列: "ap 将 "a" 寄存器的内容粘贴到光标之后,而 "bP 将 "b" 寄存器内容粘贴到之前。

“前缀”的概念,也给我们的文本操作“语言”引入了“形容词”和“副词”。大多数命令(动词)以及动作(动词或对象,依赖于上下文),也可采用数字前缀。因此, 3J 意为“合并之后三行”,而 d5} 意为“删除行直到第 5 个段落末尾”。

以上这些都是中级 vi 。没有一个是 Vim 特定的,如果你想要学习,还有更多更高级的 vi 技巧。仅掌握这些中级概念,你可能就会发现,你极少需要编写任何宏,因为该文本操作语言是如此地简洁有力,使用编辑器的“原生”语言,你就可以轻而易举地做到大多数事情。


更高级的例子:

vi 编辑器有一系列 : 命令,最有名的莫过于 :%s/foo/bar/g 全局替换(它并不高级,还有其他一堆高级 : 命令)。 : 命令从 vi 的前辈 ed (行编辑器), ex (增强行编辑器)继承而来。事实上, vi 之所以如此命名,是因为它是 ex 的可视化界面。

: 命令通常对几行文本进行操作。 edex 编写于那个终端屏幕都少见,许多终端是电传(TTY)设备的年代。通常手边放着打印稿,然后通过极其简陋的界面键入命令(连接速度一般为 100 波特,或者说每秒 11 个字符--比快速打字员还要慢些;多用户交互过程中延滞经常发生;此外,往往还有节约纸张的考虑)。

: 命令语法通常包含某一地址或地址范围(行号),后面紧跟一条命令。当然,你可以直接使用行号: :127,215s/foo/bar 将第 127 行到 215 行之间的第一处 "foo" 替换为 "bar"。你也可以使用缩写比如 .$ ,分别代表当前行和最后行。你也可以使用相对前缀 +- 分别代表当前行之后或之前的偏移。因此, :.,$j 意为从当前行到最后一行,将它们合并为一行。 :%:1,$ (所有行)意思则相同。

:... g:... v 功能异常强大,需要费一番口舌解释。 :... g 是 "globally" 的前缀,将后续命令应用于与模式(正则表达式)匹配的所有行,而 :... v 则将此命令应用于与模式不匹配的所有行("v" 源自 "conVerse")。与其它 ex 命令一样,它们也可以使用地址/范围引用来添加前缀。因此, :.,+21g/foo/d 意为从当前行到之后 21 行间删除掉包含 "foo" 的所有行。而 :.,$v/bar/d 意为从当前行到文件末尾,删除掉任何不包含 "bar" 的行。

有趣的是,常见 Unix 命令 grep 事实上也是 ex 启发而来的(其命名方式也是如此)。ex 命令 :g/re/p (grep) 正是如何 "globally" "print" 包含某 "regular expression" (re) 的方式。使用 edex 时, :p 常常是人们学习的首批命令之一,也是编辑文件时使用的第一个命令。也就是如何打印当前文本内容(通常是全部打印,偶尔使用 :.,+25p 或类似命令打印)。

注意到, :% g/.../d 或它的反面对应 :% v/.../d ,是最常使用到的模式。然而,还有其它一些值得记住的 ex 命令。

你可以使用 m 来移动行,使用 j 来合并行。比如有一个列表,你想要不经删除将所有匹配的行(或反之,不匹配的行)分离,则可以使用 :% g/foo/m$ 类似的命令,然后所有 "foo" 行将会被移动到文件末尾(注意另一个将文件末尾当作草稿区的技巧)。在将 "foo" 行分离的同时,还能保留它们原有的相对顺序(等价于类似命令: 1G!GGmap!Ggrep foo<ENTER>1G:1,'a g/foo'/d 拷贝文件到末尾,通过 grep 进行过滤,从开头删除所有东西)。

要合并行,通常我是找到要合并行的前置字符(比如,无序列表均以 ^ `` 开头而非 ``^ * 。这种情况下,我使用 :% g/^ /-1j (将匹配的行,与往上一行合并)。(顺便说下:搜索无序列表行合并到下一行,不知什么原因无法工作……将某项无序列表合并到另一个是可以的,但无法将列表项与它的所有续行合并;仅适用于成对的匹配行。)

更无需提及,你可以将先前的 s (substitute) 与 gv (global/converse-global) 命令配合使用。一般情况下,我们并不需要这么做。然而,某些时候你想仅对那些匹配的行执行替换。当然,你可以使用捕获和回溯的复杂表达式,以保留那些你不想更改的部分。然而,使用替换来分离匹配常常更加简单些: :% g/foo/s/bar/zzz/g -- 将包含 "foo" 的每一行里的 "bar" 均替换为 "zzz"( :% s/(.*foo.*)bar(.*)/1zzz2/g 命令仅对同一行中 "bar" 前面是 "foo" 的情况有用;它已经有些蹩脚难懂,如果考虑到 "bar" 在 "foo" 之前的情况,还需要对表达式进一步的包裹)。

我的意思是,在 ex 命令集中还有比 p , s , d 更有用的命令。

: 命令的地址也可引用标记。因此,你可以使用 :'a,'bg/foo/j 命令将包含 "foo" 的行合并到下一行,如果它们处于 "a" 标记与 "b" 标记之间的话(是的,之前提到的 ex 命令例子,均可用各种范围表达式进行约束)。

这看起来有些晦涩难懂(在过去的 15 年中,有些命令我仅使用过几次)。然而,我坦承我时常重复做一些繁杂的琐事,如果我早先花时间想出正确方法的话,肯定能更高效地完成。

关于 viex 另一个非常有用的命令是 :r ,读取另一个文件的内容。因此, :r foo 意即在当前行插入文件 "foo" 的内容。

还有个更强大的 :r! 命令,它读取命令执行的结果。暂停当前 vi 会话,运行一个命令,将其输出重定向到临时文件,恢复 vi 会话,从临时文件读取内容。

还有更强大的 ! (bang) 和 :... ! (ex bang) 命令。它们也是执行外部命令并将结果读取到当前文件。然而,它们也可以通过命令过滤所选择的文本!这样的话,我们就可以使用 1G!GsortGvi 的 "goto" 命令;默认是跳转到文件的最后一行,但是可以在前面加上行号,比如 1 跳转到第一行)来对文件的所有行进行排序。这等价于 ex 命令 :1,$!sort 。写作者通常使用 ! 配合 Unix 的 fmtfold 来格式化或换行选中的文本。一个常用的宏是 {!}fmt (格式化当前段落)。程序员有时用它配合 indent 或其它代码格式化工具来运行代码,或者某一部分代码,

另一个有用的 ex 命令是 :so:source 缩写)。它读取文件内容,并将其内容视为一系列命令。当你正常启动 vi 时,隐式地对 ~/.exinitrc 文件执行了 :source 命令( Vim 则通常是 ~/.vimrc )。该命令的用处是你可以通过读取一系列宏,缩略词和编辑器设置,瞬间地改变 Vim 的外观行为。你还可以将一系列 ex 命令序列存储,然后再静默地应用到想要更改的文件。

比如,有一个 7 行(36 个字符)的文件,它运行 wc 命令,并在文件开头插入包含字数统计的 C 语言风格的注释。我就可以执行 vim +'so mymacro.ex' ./mytarget 命令,将“宏”应用到该文件。

vi 以及 Vim+ 命令行选项,常被用来在指定行上开始文件编辑。然而很少有人知道,也可以在 + 后面跟任何合法的 ex 命令/表达式,正如我在此处使用的 "source" 命令;另一个简单例子是: vi +'/foo/d|wq!' ~/.ssh/known_hosts 从 SSH 已知主机里静默地删除某条目。)

通常来说,使用 Perl, AWK, sed (与 grep 一样由 ed 启发的工具)写出这样的“宏”,可一点都不容易。

@ 可能是 vi 里最晦涩的命令。在将近 10 年的时常给高级系统管理员授课的过程中,我没碰见几个人使用它。 @viex 命令一样,执行寄存器里的内容。

例子:我常使用 :r!locate ... 查找系统里的文件,并读取其文件名称。删除掉所有无关命中,仅留下感兴趣的文件的完整路径。与其不辞劳苦地按 Tab 遍历文件路径(或者更糟,我登陆的机器上碰巧没有 Tab 补全支持),我是这样做的:

  1. 0i:r (把当前行变为合法的 :r 命令);

  2. :cdd (删除行并存入 "c" 寄存器),然后

  3. @c 执行命令

仅需要 10 次键击(表达式 "cdd @c 于我而言,是一指之隔的效率宏,因此我能将这 6 个字符像其它常见字符一样快速地键入)。


一点警醒

此处对 vi 强大之处的介绍,还仅仅是浮光掠影。我在这里所讲的,甚至没有一点是 vim “改进”(vim 名称由此而来)的地方!我在这里讲的所有东西,均可以在 20 至 30 年前的 vi 上正常运行。

还有很多人对 vi 的使用,比我更出神入化。