Vim 的问题在于你不了解 vi。
您提到使用yy 进行剪切,并抱怨您几乎从不想剪切整行。事实上,编辑源代码的程序员经常希望在整行、行范围和代码块上工作。但是,yy 只是将文本拉入匿名复制缓冲区(或在 vi 中调用的“注册”)的众多方法之一。
vi 的“禅”是你在说一种语言。开头的y 是动词。声明yy 是y_ 的同义词。 y 被加倍以便于输入,因为它是一种常见的操作。
这也可以表示为ddP(删除当前行并将副本粘贴回原处;在匿名寄存器中留下副本作为副作用)。 y 和d“动词”将任何动作作为它们的“主语”。因此yW 是“从这里(光标)拉到当前/下一个(大)单词的末尾”,y'a 是“从这里拉到包含名为 'a的标记的行>'。”
如果您只了解基本的向上、向下、向左和向右光标移动,那么 vi 对您来说不会比“记事本”副本更有效率。 (好吧,您仍然可以使用语法高亮和处理大于约 45KB 左右的文件的能力;但请在此处与我合作)。
vi 有 26 个“标记”和 26 个“寄存器”。使用m 命令将标记设置到任何光标位置。每个标记由一个小写字母指定。因此ma 将'a' 标记设置为当前位置,mz 设置'z' 标记。您可以使用'(单引号)命令移动到包含标记的行。因此'a 移动到包含'a' 标记的行的开头。您可以使用`(反引号)命令移动到任何标记的精确位置。因此 `z 将直接移动到“z”标记的确切位置。
因为这些是“运动”,它们也可以用作其他“陈述”的主题。
因此,剪切任意文本选择的一种方法是删除一个标记(我通常使用 'a' 作为我的“第一个”标记,'z ' 作为我的下一个标记,'b' 作为另一个标记,以及 'e' 作为另一个标记(我不记得在 15 年中交互使用过超过四个标记使用 vi;一个人创建自己的约定,关于宏如何使用标记和寄存器,不会干扰一个人的交互式上下文)。然后我们转到所需文本的另一端;我们可以开始在任何一端都没有关系。然后我们可以简单地使用d`a 剪切或y`a 复制。因此整个过程有5 次击键开销(如果我们以“插入”模式开始并且需要Esc out 命令模式)。一旦我们已经剪切或复制,然后粘贴复制是一个单一的按键: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 的可视界面。
: 命令通常在文本行上运行。 ed 和 ex 是在终端屏幕不常见并且许多终端是“电传打字机”(TTY)设备的时代编写的。因此,从文本的打印副本开始工作是很常见的,通过极其简洁的界面使用命令(常见的连接速度为 110 波特,或者大约每秒 11 个字符 - 这比快速打字员慢;滞后在多用户互动会话;此外,通常还有一些节省纸张的动机)。
所以大多数: 命令的语法包括一个地址或地址范围(行号),后跟一个命令。自然可以使用文字行号::127,215 s/foo/bar 在 127 到 215 之间的每一行上将第一次出现的“foo”更改为“bar”。也可以使用一些缩写,例如 . 或 $ 表示当前和最后一行分别。也可以使用相对前缀+ 和- 分别表示当前行之后或之前的偏移量。因此::.,$j 意思是“从当前行到最后一行,将它们全部连接成一行”。 :% 是 :1,$ 的同义词(所有行)。
:... g 和 :... v 命令有一些解释,因为它们非常强大。 :... g 是“全局”的前缀,将后续命令应用于与模式(正则表达式)匹配的所有行,而 :... v 将此类命令应用于与给定模式不匹配的所有行(“conVerse”中的“v” ”)。与其他 ex 命令一样,这些命令可以以寻址/范围引用作为前缀。因此:.,+21g/foo/d 表示“从当前行到接下来的 21 行中删除包含字符串“foo”的任何行”,而:.,$v/bar/d 表示“从这里到文件末尾,删除任何不包含字符串的行“酒吧。”
有趣的是,常见的 Unix 命令 grep 实际上是受此 ex 命令的启发(并以其记录方式命名)。 ex 命令:g/re/p(grep)是他们记录如何“全局”“打印”包含“正则表达式”(re)的行的方式。当使用 ed 和 ex 时,:p 命令是任何人最先学会的命令之一,并且通常是在编辑任何文件时使用的第一个命令。这是您打印当前内容的方式(通常使用:.,+25p 或类似的方式一次只打印一页)。
请注意,:% g/.../d 或(它的反向/conVerse 对应物::% 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(替代)与g 和v(全局/converse-global)命令一起使用。通常你不需要这样做。但是,考虑某些情况,您只想对匹配其他模式的行执行替换。通常,您可以使用带有捕获的复杂模式并使用反向引用来保留您不想更改的行部分。但是,将匹配项与替换项分开通常会更容易::% g/foo/s/bar/zzz/g——对于包含“foo”的每一行,将所有“bar”替换为“zzz”。 (像:% s/\(.*foo.*\)bar\(.*\)/\1zzz\2/g 之类的东西只适用于那些在同一行上由“foo”之前的“bar”实例的情况;它已经够难看的了,必须进一步处理以捕捉所有“bar”的情况" 在 "foo" 之前)
关键在于ex 命令集中的行不只是p、s 和d。
: 地址也可以引用标记。因此,您可以使用::'a,'bg/foo/j 将包含字符串 foo 的任何行连接到其后续行,如果它位于 'a' 和 'b' 之间的行之间分数。 (是的,前面的所有ex 命令示例都可以通过使用这些寻址表达式作为前缀来限制为文件行的子集)。
这很模糊(在过去的 15 年中,我只使用过几次类似的东西)。但是,我坦率地承认,我经常以迭代和交互方式完成一些事情,如果我花时间思考正确的咒语,这些事情可能会更有效地完成。
另一个非常有用的 vi 或 ex 命令是:r,用于读取另一个文件的内容。因此::r foo 在当前行插入名为“foo”的文件的内容。
更强大的是:r! 命令。这将读取命令的结果。这与暂停 vi 会话、运行命令、将其输出重定向到临时文件、恢复您的 vi 会话以及从临时文件中读取内容相同。文件。
更强大的是! (bang) 和:... ! (ex bang) 命令。它们还执行外部命令并将结果读入当前文本。但是,它们还通过命令过滤我们的文本选择!我们可以使用1G!Gsort 对文件中的所有行进行排序(G 是 vi "goto" 命令;它默认转到文件的最后一行,但可以添加前缀行号,例如 1,第一行)。这相当于 ex 变体 :1,$!sort。作者经常将! 与Unix fmt 或fold 实用程序一起使用,以重新格式化或“自动换行”文本选择。一个非常常见的宏是{!}fmt(重新格式化当前段落)。程序员有时通过 indent 或其他代码重新格式化工具使用它来运行他们的代码,或者只是其中的一部分。
使用:r! 和! 命令意味着任何外部实用程序或过滤器都可以被视为我们编辑器的扩展。我偶尔会将这些与从数据库中提取数据的脚本一起使用,或者与从网站中提取数据的 wget 或 lynx 命令或 ssh 一起使用从远程系统中提取数据的命令。
另一个有用的ex 命令是:so(:source 的缩写)。这会将文件的内容作为一系列命令读取。当您启动 vi 时,它通常会隐式地在 ~/.exinitrc 文件上执行 :source(而 Vim 通常会在 ~/.vimrc 上执行此操作,这很自然)。这样做的用途是,您可以通过简单地获取一组新的宏、缩写和编辑器设置来即时更改您的编辑器配置文件。如果您很狡猾,您甚至可以将其用作存储 ex 编辑命令序列以按需应用于文件的技巧。
例如,我有一个七行文件(36 个字符),它通过 wc 运行一个文件,并在包含该字数数据的文件顶部插入一个 C 样式的注释。我可以使用如下命令将该“宏”应用于文件:vim +'so mymacro.ex' ./mytarget
(vi 和 Vim 的+ 命令行选项通常用于在给定的行号处启动编辑会话。然而,鲜为人知的事实是可以通过任何有效的 ex 命令/表达式跟随+,例如我在这里所做的“源”命令;举个简单的例子,我有脚本可以调用:vi +'/foo/d|wq!' ~/.ssh/known_hosts在我重新映像一组服务器时,以非交互方式从我的 SSH 已知主机文件中删除一个条目)。
通常使用 Perl、AWK、sed 编写这样的“宏”要容易得多(实际上,就像 grep 一个受 启发的实用程序ed 命令)。
@ 命令可能是最不起眼的 vi 命令。在近十年间偶尔教授高级系统管理课程的过程中,我遇到的人很少使用它。 @ 执行寄存器的内容,就好像它是 vi 或 ex 命令。
示例:我经常使用::r!locate ... 在我的系统上查找某个文件并将其名称读入我的文档中。从那里我删除任何无关的点击,只留下我感兴趣的文件的完整路径。而不是费力地 Tab 遍历路径的每个组件(或者更糟,如果我碰巧vi 的副本中卡在没有 Tab 补全支持的机器上)我只是使用:
-
0i:r(将当前行变成有效的:r命令),
-
"cdd(将行删除到“c”寄存器中)和
-
@c 执行该命令。
这只是 10 次击键(而表达式 "cdd @c 对我来说实际上是一个手指宏,所以我几乎可以像输入任何常见的六个字母单词一样快速地输入它)。
发人深省的想法
我只是触及到了 vi 强大功能的表面,我在这里所描述的甚至都不是 vim 的“改进”的一部分命名!我在这里描述的所有内容都应该适用于 20 或 30 年前的任何旧版 vi。
有些人使用 vi 的力量比我以往任何时候都多。