如何使用 Vim 才能体现出 Vim 的效率?

图0:如何使用 Vim 才能体现出 Vim 的效率?

我听过很多关于 Vim 的评价,既有优点也有缺点。作为开发者,使用 Vim 确实比其他编辑器更快。但我目前只用 Vim 处理一些基础任务,使用效率最多只能达到其他编辑器的十分之一。

当谈论速度时,你应该关注的只有两件事(你可能不够重视它们,但你应该重视):

  1. 交替使用左右手是使用键盘的最快方式。
  2. 完全不使用鼠标是实现最快速度的第二种方式。移动手部、抓取鼠标、移动鼠标再将其放回键盘需要花费大量时间(而且你经常需要查看键盘以确保手部已正确返回正确位置)

以下是两个示例,说明为何我在使用Vim时效率远低于预期。

元素周期表

复制/剪切和粘贴。 我经常做这些操作。在所有现代编辑器中,你用左手按下 Shift 键,然后用右手移动光标来选择文本。然后按 Ctrl+C 复制,移动光标,再按 Ctrl+V 粘贴。

在 Vim 中,这非常糟糕:

  • yy 复制一行(你几乎不会想要整行!)。
  • [number xx]yy 复制 xx 行到缓冲区。但你永远无法确定是否选中了想要的内容。我经常不得不执行 [number xx]dd 然后 u 撤销!

另一个例子?搜索与替换。

  • PSPad 中:按 Ctrl+f,输入要搜索的内容,然后按 Enter。
  • 在 Vim 中:输入 /,然后输入要搜索的内容,如果遇到特殊字符,需在每个特殊字符前加上 ,然后按 Enter。

Vim 的所有操作都类似于此:我似乎不知道如何正确处理它。

我的问题是:

你使用 Vim 的方式是什么,让你比使用现代编辑器更高效?

解惑:你使用 Vim 的问题在于你没有真正理解 vi

你提到使用 yy 剪切,并抱怨几乎从未需要剪切整行。事实上,程序员在编辑源代码时,经常需要处理整行、行范围和代码块。然而,yy只是将文本复制到匿名复制缓冲区(或在vi中称为“寄存器”)的多种方式之一。

vi的“禅意”在于,你是在使用一种语言。初始的 y 是一个动词。语句 yyy_ 的同义词。y 被重复使用以方便输入,因为这是一个非常常见的操作。

这也可以表示为 dd P(删除当前行并将其副本粘贴回原位;作为副作用,副本保存在匿名寄存器中)。“y”和“d”这两个“动词”可以作为任何移动的“主语”。因此,“yW”是“从这里(光标)到当前/下一个(大)单词的末尾进行剪切”,而“y’a”是“从这里剪切到包含名为‘a’的标记的那一行”。

如果你只了解基本的向上、向下、向左和向右光标移动,那么 vi 对你来说就和“记事本”一样没有效率了。(好吧,你仍然可以使用语法高亮显示功能,并且能够处理大于 45KB 左右的小文件;但请先听我讲完)。

vi 有 26 个“标记”和 26 个“寄存器”。使用 m 命令将标记设置到任何光标位置。每个标记由一个小写字母表示。因此,ma 将“a”标记设置到当前位置,而 mz 将“z”标记设置到当前位置。你可以使用 (单引号)命令移动到包含标记的行。例如,'a 会移动到包含 ‘a’ 标记的行开头。你可以使用 `(反引号)命令移动到任何标记的精确位置。例如,`z 会直接移动到 ‘z’ 标记的精确位置。

由于这些是“移动”操作,它们也可以作为其他“语句”的操作对象。

因此,切割任意文本选区的一种方法是设置标记(我通常使用 ‘a’ 作为第一个标记,‘z’ 作为下一个标记,‘b’ 作为另一个,‘e’ 作为另一个(我记得在使用 vi 的 15 年中,从未在交互模式下使用过超过四个标记; 用户可以自行定义标记和寄存器在宏中的使用方式,以避免干扰交互式上下文)。然后我们移动到所需文本的另一端;我们可以从任一端开始,这并不重要。然后我们可以简单地使用d`a来剪切或y`a来复制。因此整个过程需要5个键盘输入(如果从“插入”模式开始并需要退出命令模式,则需要6个)。一旦剪切或复制完成,粘贴副本只需一个键盘输入:p

我认为这是剪切或复制文本的一种方法。然而,这只是众多方法中的一种。通常,我们可以更简洁地描述文本范围,而无需移动光标并放置标记。例如,如果我处于一段文本中,我可以使用 {} 分别移动到段落的开头或结尾。因此,要移动一段文本,我可以使用 { d}(3个键盘敲击)进行剪切。(如果我恰好位于段落的第一行或最后一行,则可以分别使用 d}d{。)

“段落”的概念默认指通常直观合理的文本块。因此它既适用于代码也适用于散文。

我们常知道一些模式(正则表达式)可标记感兴趣文本的起始或结束位置。在vi中,向前或向后搜索即为移动操作。因此它们也可作为“语句”中的“主体”。因此,我可以使用 d/foo 从当前行剪切到下一行包含字符串 “foo” 的行,以及使用 y?bar 从当前行复制到最近(上一行)包含 “bar” 的行。如果我不希望剪切整行,仍可使用搜索移动(作为独立的语句),取消标记,并使用之前描述的 `x 命令。

除了“动词”和“主语”外,vi 还具有“对象”(在语法意义上)。到目前为止,我只描述了匿名寄存器的使用。然而,我可以通过在“对象”引用前加上 "(双引号修饰符)来使用任何 26 个“命名”寄存器。因此,如果我使用 “add,则将当前行复制到 ‘a’ 寄存器中;如果使用 ”by/foo,则将从当前位置到下一个包含 “foo” 的行中的文本复制到 ‘b’ 寄存器中。要从寄存器粘贴,只需在粘贴前加上相同的修饰符序列:“ap 将“a”寄存器内容的副本粘贴到光标后的文本中,而 ”bP 将“b”的副本粘贴到当前行之前。

这种“前缀”的概念也为我们的文本操作“语言”增添了类似语法中的“形容词”和“副词”的元素。大多数命令(动词)和移动操作(根据上下文可能是动词或对象)也可以带数字前缀。因此 3J 表示“合并接下来的三行”,而 d5} 表示“从当前行删除至从这里往下数第五段的结尾”。

这些都是 vi 的中级水平。其中没有任何内容是 Vim 专有的,如果准备好学习,vi 中还有更多高级技巧。如果你能掌握这些中级概念,你可能会发现很少需要编写宏,因为文本操作语言足够简洁和表达力强,可以轻松地使用编辑器的“原生”语言完成大多数任务。


更高级技巧的示例:

有许多 : 命令,最值得注意的是 :% s/foo/bar/g 全局替换技术。(这不算高级,但其他 : 命令可以是。)整个 : 命令集历史悠久,源自 vi 的前身——作为 ed(行编辑器)和后来 ex(扩展行编辑器)工具的继承。事实上,vi之所以得名,是因为它是ex的可视化界面。

:命令通常作用于文本行。edex诞生于终端屏幕尚未普及的时代,当时许多终端是“电传打字机”(TTY)设备。因此,人们通常使用打印版的文本进行操作,通过极简的界面输入命令(当时常见的连接速度为 110 波特,即大约每秒 11 个字符——这比快速打字的速度还慢;多用户交互会话中常出现延迟;此外,为了节省纸张,也常有此类需求)。

因此,大多数 : 命令的语法包括一个地址或地址范围(行号), followed by a command. 当然可以使用字面行号::127,215 s/foo/bar 表示将第127行至第215行中“foo”的首次出现替换为“bar”。也可使用缩写如.$分别表示当前行和上一行。还可以使用相对前缀 +- 分别表示当前行之后或之前的偏移量。例如::.,$j 表示“从当前行到最后一行,将所有行合并为一行”。:%:1,$(所有行)是同义的。

:... g:... v 命令需要一些解释,因为它们非常强大。:... g 是“全局应用”的缩写,用于将后续命令应用于所有匹配特定模式(正则表达式)的行,而 :... v 则将此类命令应用于所有不匹配给定模式的行(“v”来自“conVerse”)。与其他 ex 命令类似,这些命令可以使用地址/范围引用作为前缀。因此,:.,+21g/foo/d 表示“从当前行开始,删除包含字符串 ‘foo’ 的接下来的 21 行”,而 :.,$v/bar/d 表示“从当前位置到文件末尾,删除不包含字符串 ‘bar’ 的所有行”。

有趣的是,常见的 Unix 命令 grep 实际上是受此 ex 命令启发而创建的(其名称源自文档中的描述方式)。ex 命令 :g/re/p(grep)是他们用来描述如何“全局”打印包含“正则表达式”(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(替换)命令,配合gv(全局/反全局)命令。通常无需这样做。然而,考虑一些情况下,你希望仅对匹配其他模式的行进行替换。通常可以使用包含捕获的复杂模式,并通过反向引用保留你不希望更改的行部分。然而,将匹配与替换分离通常更简单::% g/foo/s/bar/zzz/g——对每行包含“foo”的行,将所有“bar”替换为“zzz”。(类似于 :% s/(.*foo.*)bar(.*)/1zzz2/g 的表达式仅适用于那些在同一行中由 “foo” 先行出现的 “bar” 实例;这种表达式已经足够笨拙,而且还需要进一步修改才能捕获所有 ‘bar’ 先行于 “foo” 的情况)

重点是,ex命令集中的行不仅仅包括psd

:地址还可以引用标记。因此你可以使用::'a,'bg/foo/j 将包含字符串foo的行与后续行连接,如果该行位于’a‘和’b‘标记之间的行之间。(是的,前面所有 ex 命令示例都可以通过在前面添加此类地址表达式来限制为文件行的一部分。)

这相当晦涩(我过去 15 年中只用过几次类似的东西)。然而,我坦率承认,我经常以迭代和交互的方式完成一些任务,如果我花时间思考正确的命令,这些任务本可以更高效地完成。

另一个非常有用的 viex 命令是 :r,用于读取另一个文件的内容。例如::r foo 将文件 “foo” 的内容插入到当前行。

更强大的是:r!命令。该命令读取命令的执行结果。其效果等同于暂停vi会话、执行命令、将输出重定向到临时文件、恢复vi会话,然后从临时文件中读取内容。

更强大的是 !(感叹号)和 :... !(ex 感叹号)命令。这些命令同样可以执行外部命令并将其结果读入当前文本。然而,它们还会通过命令过滤我们文本中的选定内容!我们可以使用 1G!Gsort(Gvi 的“跳转”命令;默认跳转到文件的最后一行,但可以前缀行号,如 1,表示第一行)对文件中的所有行进行排序。这与 ex 变体 :1,$!sort 效果相同。作家常使用 ! 配合 Unix 的 fmtfold 工具来重新格式化或“换行”选定文本。一个非常常见的宏是 {!}fmt(重新格式化当前段落)。程序员有时会用它来运行代码(或代码的一部分)通过 indent 或其他代码格式化工具。

使用 :r!! 命令意味着任何外部工具或过滤器都可以被视为编辑器的扩展。我偶尔会将这些命令与从数据库提取数据的脚本一起使用,或与从网站提取数据的 wgetlynx 命令一起使用,或与从远程系统提取数据的 ssh 命令一起使用。

另一个有用的 ex 命令是 :so(即 :source 的缩写)。该命令将文件内容作为一系列命令读取。当你正常启动 vi 时,它会隐式地对 ~/.exinitrc 文件执行 :source 操作(而 Vim 通常会对 ~/.vimrc 文件执行此操作,这很自然)。此功能的用途在于,您可以通过加载一组新的宏、缩写和编辑器设置,随时更改编辑器配置。如果您足够狡猾,甚至可以将此作为存储ex编辑命令序列的 trick,以便按需应用于文件。

例如,我有一个七行文件(36个字符),该文件会将文件通过wc命令处理,并在文件顶部插入一个包含字数统计数据的C风格注释。我可以通过以下命令将该“宏”应用到文件:vim +‘so mymacro.ex’ ./mytarget

(+ 命令行选项在 viVim 中通常用于从指定行号开始编辑会话。然而,一个鲜为人知的事实是,可以在 + 之后跟上任何有效的 ex 命令/表达式,例如我在此处使用的“source”命令;对于一个简单示例,我有脚本调用:vi +'/foo/d|wq!' ~/.ssh/known_hosts,以在重新映像一组服务器时非交互式地从 SSH 已知主机文件中删除一条条目)。

通常,使用 Perl、AWK 或 sed(实际上,sedgrep 一样,是受 ed 命令启发的工具)编写此类“宏”要容易得多。

@ 命令可能是 vi 中最不为人知的命令。在近十年的高级系统管理课程教学中,我遇到的使用过该命令的人寥寥无几。@ 命令会将寄存器中的内容作为 viex 命令执行。
示例: 我经常使用::r!locate ... 在系统中查找某个文件并将其名称读入文档。随后删除多余的匹配结果,仅保留感兴趣文件的完整路径。与其费力地通过 Tab 键逐个输入路径组件(更糟糕的是,如果我恰好使用的是不支持 Tab 补全的 vi 版本),我只需使用:

  1. 0i:r(将当前行转换为有效的**:r**命令),
  2. "cdd(将该行删除并保存在“c”寄存器中),
  3. @c执行该命令。

这只需10个键盘敲击(而表达式"cdd @c对我来说相当于一个手指宏,因此我可以几乎与任何常见的六字母单词一样快速地输入它)。


令人深思的观点

我只是触及了vi功能的表面,而我在此描述的任何内容都并非vim命名所指的“改进”的一部分!我在此描述的所有内容都应能在20或30年前的任何旧版vi上运行。

有的人使用vi的强大功能比我多得多。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

链接收藏


京ICP备12002735号