VI编辑器简介
1. 概述
在数字时代完成工作在很大程度上取决于我们处理文本数据的速度和效率。
在本教程中,我们将采用老式的方法来了解传统的 vi 编辑器 作为一种简约而强大的文本编辑工具。
2. 传统vi
我们将**探索的范围限制在传统的 vi 编辑器 **中。考虑到这一点,让我们从了解 vi 与ex 行编辑器的联系开始。
2.1. 设置
长期以来,vi 的功能集一直是Single Unix Specification 的一部分,使其成为大多数 Linux 发行版中的默认编辑器。但是,目前,大多数操作系统通过其克隆为Vim (Vi IMproved) 提供 vi 支持:
ls -l $(which vi)
lrwxr-xr-x 1 root wheel 3 Jan 9 05:03 /usr/bin/vi -> vim
所以,每当我们调用vi时,我们实际上是在调用vim。
为了确保所有在 vi 中工作的东西继续在 Vim 中工作,我们可以选择使用 Vim 的兼容模式*(-C)*:
$ vim -C
但是,作为一个增强的克隆,Vim 自然地提供了比传统的 vi 编辑器更广泛的功能集。而且,兼容模式并不限制我们在 Vim 中使用那些互斥的特性。
现在,从功能的角度来看,这一切都很好。但是,我们最终可能会使用非 vi 功能并假设它是 vi 功能。
为了避免这种混淆,并明确 vi 编辑器支持的功能,我们将利用**预装 vi 的即用型虚拟Docker **环境。
所以,让我们运行我们的虚拟环境并开始工作:
$ docker run -it tapankavasthi/vi
blogdemo@itcodingman:/tmp/vi/work#
2.2. vi是ex
在我们的虚拟容器中,让我们使用file 命令检查vi可执行二进制文件:
# file $(which vi)
/usr/local/bin/vi: symbolic link to ex
正如我们所见,** vi可执行文件是ex二进制文件的符号链接。**因此,很明显,支持vi的底层程序是ex。
2.3. ex模式
让我们创建一个示例hello.txt文件并使用ex命令打开它:
# printf "Line-1\nLine-2\n" > list.txt
# /usr/local/bin/ex list.txt
令人惊讶的是,一旦打开文件,我们就看不到文件的内容。另一方面,我们看到一个冒号 ( : ) 命令提示符:
"hello.txt" 2 lines, 14 characters
:
因此,这是 ex 的默认模式,我们希望在冒号提示符处运行 ex-commands。所以,让我们试试以*%符号为前缀的p*(打印)命令:
"hello.txt" 1 line, 14 characters
:%p
Line-1
Line-2
:
2.4. ex的视觉模式
如果我们看一下ex的命令行用法,那么我们可以发现一个*-v*(视觉)选项:
Usage: ex [- | -s] [-l] [-L] [-R] [-r [file]] [-t tag]
[-v] [-V] [-w size] [+cmd | -c cmd] file...
让我们使用q (quit) 命令退出最后打开的文件,然后使用*-v*选项重新打开它:
$ /usr/local/bin/ex -v list.txt
有趣的是,我们现在可以在文件打开后立即看到文件的内容。好吧,它被正确地称为视觉模式:
Line-1
Line-2
~
~
~
"list.txt" 2 lines, 14 characters
或者,我们可以直接使用vi命令调用 ex 的可视模式:
$ vi list.txt
所以,我们可以说vi是ex -v。
3. 我看到vi模态编辑器
当我们使用vi命令打开文件时,我们进入 ex 的可视模式。但是,从 vi 的角度来看,这是默认模式。
我们可以在此模式下使用 ex 命令的子集进行编辑。因此,它通常被称为命令模式。vi 还支持插入模式,在这种模式下,大多数击键转换为在文件中键入文本。
而且,我们可以分别通过按Esc或键入i在命令模式和插入模式之间轻松切换。
最后,vi 还提供了对 ex-like 模式的轻松访问,我们可以在冒号提示符下输入命令而不会丢失视觉上下文。
现在,当涉及到切换这些模式时,最好通过视觉方法来理解这一点:
作为一个快速练习,让我们使用这个循环流程来弄清楚我们如何从 ex 的冒号提示符转到插入模式。
4. 视觉模式的语言
在每种模式下,vi 都理解一组特定的指令。为了在视觉模式下有效地工作,让我们从学习它的语言概念开始。
4.1. 语法基础
一般来说,我们需要按照特定的顺序向vi提供编辑指令:
[Modifier][Operator][Operand]
为了澄清,让我们看一个删除三个字符的简单指令:
因此,字符、单词和行形式的文本对象用作操作数。另一方面,运算符执行文本处理,例如更改(c)、删除(d)、复制(y)、替换(r)等。最后,诸如数字计数之类的修饰符可以修改运算符的原始行为。
从本质上讲,文本编辑非常类似于求解一个数学方程,我们可以在其中得到一个问题的多个解决方案。但是,在这种情况下,我们应该尽量减少击键次数。而且,这就是为什么在使用 vi 时单次击键非常普遍的原因之一。
4.2. 常用操作数
当我们处理文本时,vi 理解三大类操作数;即字符、单词和行。
**所以,听起来很简单吧?但是,魔鬼睡得很详细,**尤其是对于文字的概念。
让我们从掌握字符的概念开始。好吧,字符操作数是使用参考点来识别的,例如光标的当前位置或行首:
- 对于当前行,0和*$*表示开始和结束
- 关于光标位置,h和l分别表示左右字符
- ^和_,都标识第一个非空白字符
- N| 表示第N列的字符, N的缺失值被解释为1
现在,让我们拼出用于单词的 vi 定义:
- 其中第一个是词汇( W ),一个连续的非空白字符序列,后面总是跟着一个空格*[ ^I]**或行尾字符 ( $ )
- 而且,第二个是逻辑字 (w),它总是以非空白字符开头,后跟*[a-zA-Z0-9]*的序列*
此外,让我们了解一些从单词操作数派生的字符标识符:
最后,我们还看一下一些特定于行的操作数:
4.3. 常用运算符
由于操作数带有关联字符的明确位置标识,因此当我们没有明确指定运算符时,光标移动是适用于它们的默认运算符。因此,当我们处于命令模式时,按下操作数对应的键会将光标移动到操作数标识的位置。
但是,还有一些其他的操作符要求我们明确地表达我们的意图,以防止对文本的意外修改。让我们看看其中的几个:
到目前为止,一切都很好。现在,让我们看一下两个运算符,它们给人一种我们处于插入模式的感觉:
我们可以看到它将我们带入了**插入模式的范围版本,其中*$符号表示替换文本的预期软边界。**而且,要确认更改并返回正常的视觉模式,我们必须按Esc*键。
4.4. 经营范围
对于dw、d/search_word等编辑操作,我们从左到右逐个字符地遍历。所以,我们的操作范围是字符级别的。 但是,当涉及到诸如dj、ck或yj之类的编辑操作时,我们是在行级别上操作的。
在这种情况下,位于当前光标位置和j或k目标字符位置之间的文本跨越两行。因此,对于这些操作,结果是删除、更改或复制两行:
4.5. 重复
很多时候,文本编辑涉及重复性工作,例如对多个字符、单词或行应用相同的操作。而且,vi 通过它的两个产品为我们提供了便利:
- 点 ( . )运算符重新运行最后的编辑操作
- 一个命令之前的数字限定符对许多后续操作数重复它
假设我们有一些代码需要适当的制表符缩进。当然,*»和«*运算符在这里会派上用场。
因此,如果我们必须通过单个制表符缩进多个连续的行,那么我们可以在*»*之前使用数字量词:
现在,我们有另一种情况,我们希望同一行有多个制表符缩进。所以,这里我们可以使用*»或«一次,后跟点*运算符重复前面的命令执行:
5. 上下文管理
为了便于导航和上下文管理,vi 提供了标记和寄存器。让我们详细了解它们。
5.1. 标记
文本编辑涉及从一行到另一行**的大量光标移动。**不幸的是,在这样做的时候,我们可能会忘记我们当前的上下文。因此,为了解决这个问题,vi 让我们标记当前光标位置以备后用。
要使用标记,让我们首先熟悉与它们相关的重要键绑定:
- mλ 创建一个标记——其中 λ 是a到z之间的任何小写字母
- ‘λ标识标记行中的第一个非空白字符**
- `λ标识行上进行标记的确切位置
- ‘`标识光标上一行位置上的第一个非空白字符
- `‘标识光标在上一行位置上的准确位置
假设我们有一个包含两个部分的文件,即诗歌正文及其引用。让我们看看如何通过用mp和mr标记这两个部分来在它们之间导航:
现在,如果在某个时间点,我们不需要文件中的引用列表,那么我们可以在我们的编辑操作*G$d’r *中使用这个标记信息:
5.2. 寄存器
在大多数文本编辑环境中,我们有一个剪贴板的概念,我们可以在其中复制一些内容以供以后使用。但是,使用 vi,我们得到了多个这样的占位符桶,称为 registers。
因此,我们不能将寄存器与变量混淆。虽然变量名通常是用户定义的,但有一组预定义的寄存器,我们不能重命名它们。所有寄存器名称都以*"*字符开头,后跟一个字符。
自然,寄存器支持文本传输操作,例如yank (y)、delete (d)、cut (x)和paste (p):
<register-name><text-transfer-operator>
因此,“5yy”操作会将行复制到数字寄存器“5”中,当需要时,我们可以执行“5p ”或*“5P”之*类的操作。
尽管这适用于简单的用例,但是,我们应该**避免显式覆盖数字寄存器*“0-9”***中的内容。这是因为 vi 使用数字寄存器作为保存我们最后十个文本传输操作中的文本的一种方式:
Text Transfer -> "0 -> "1 -> "2 -> "3 -> "4 -> "5 -> "6 -> "7 -> "8 -> "9 -> Lost
即使我们明确使用寄存器,也会发生这种文本传输序列。因此,当我们明确写入数字寄存器时,vi 总是有机会干扰我们的工作流程。
好吧,在 vi 寄存器池中,“az和”AZ被称为命名寄存器**。关于这些寄存器需要注意的重要一点是,每个小写命名寄存器及其对应的大写命名寄存器都指向同一个存储位置。但是,它们在将文本移入其中时表现出不同的行为:
- 小写命名寄存器*“a – z”*用移入其中的新内容替换旧内容
- 大写命名寄存器*“A* – ”Z*用换行符将新内容附加到以前的内容
现在,假设我们正在处理一个包含 HTTP 和 HTTPS URI 混合的文件。我们的目标是隔离他们。有趣的是, vi 中的删除操作更像是一个剪切操作,并且可以从寄存器中保存和检索数据。因此,让我们草拟一个策略来解决我们的用例删除操作和寄存器:
- 寄存器*“S”和“U”*可分别用于HTTPS和HTTP URI列表
- register dd操作可用于将 URI 信息临时传输到寄存器
- 要擦除寄存器中的旧数据,第一次删除可以使用小写的命名寄存器
- 标记s和u将用于为 HTTPS 和 HTTP URI 的隔离列表标记位置上下文
6. ex模式
因为 ex 最初是一个行编辑器实用程序。因此,当我们的编辑任务涉及行级操作时,ex 模式可以证明是非常有用的。让我们准备好在冒号提示符下执行一些命令。
6.1. 地址基础
在 ex 模式下,我们需要提供要执行特定命令的行地址。出于这个原因,大多数 ex 的编辑命令都需要一个address_range前缀:
:<address_range> <editing-command>
此外,我们可以以多种格式指定地址范围:
- **以数字行号、当前行 ( . )或最后一行 ($) **形式的绝对地址
- 相对于当前行的相对地址,表示为方向 ( + , – ) 和用于计算行步数的正数或负数
- 作为正则表达式/ regex /
- 上述三种类型中任何一种的两个地址值的逗号分隔值
- 用%*符号表示的全范围线
我们必须注意,如果我们不指定地址,那么默认情况下,该操作将只针对当前行 (.) 执行。
我们必须注意,当前行在每次地址评估后都会发生变化。因此,可以在没有p ( print ) 命令的情况下使用此行为来转到地址范围的最后一行:
:<address-range>
6.2. 常用编辑命令
虽然p(命令)在尝试理解地址的概念时很方便,但当我们有一个多行可视屏幕时,我们并不真正需要它。所以,让我们花点时间来亲身体验一下 ex 模式下一些常用的编辑命令。
首先,让我们看一些简单的命令,例如删除(d)、移动(m):
- [address_range] d [register],可用于将行从地址范围移动到寄存器
- [address_range] y [register],可用于将行从地址范围拉到寄存器
- [address_range] pu [register],可用于将寄存器的内容放到指定地址范围之后
- [address_range] j,可用于连接落在指定地址的行
- [address_range] m target_address,可用于在target_address之后放置一系列行
- [address_range] co target_address,可用于复制target_address之后的一系列行
接下来,让我们看一下替换命令(s),因为它可能是最常用的ex命令之一:
:[address_range] s/pattern/replacement/[[g|count][cp]]
顾名思义,** s命令在给定地址范围内搜索模式,然后用替换字符串替换它**。默认情况下,每行只进行一次替换,但我们可以更改此行为:
- 使用g ( global ) 标志,每行的所有匹配都被替换
- count数值限制每行的替换次数
- c ( confirm )标志在进行替换之前提示确认
- 使用p ( print ) 标志,打印最后一次成功替换的行
现在,假设我们在文件中有一个项目名称列表,每行一个。而且,我们的目标是将其转换为以逗号分隔的单行列表。让我们看看如何使用*替换 (s)和连接 (j)*命令来做到这一点:
我们必须注意,键在不同的模式下可以执行不同的操作,例如j在命令模式下将光标向下移动一行,但在 ex 模式下执行连接。
6.3. 重复
使 vi 如此强大的功能之一是能够轻松地重复一个动作。而且,要在 ex –模式下重复执行编辑操作,我们可以使用g ( global ) 命令:
:[address_range]g/pattern/cmd
默认情况下,ex-mode 中的任何编辑命令都将在整个地址范围内执行一次。但是,*当与*global ( g)命令结合使用时,一个命令将对地址范围内与模式匹配的每一行执行一次。
让我们重新审视 URI 隔离问题,我们必须隔离 HTTP 和 HTTPS URI 的混合列表,但这一次,我们将在 ex-mode 中解决它:
重复命令类别中的另一个是&命令,它允许我们重复最后一个替换命令。
因此,让我们用它来确定仅由“(”和“)”字符组成的给定表达式是否满足平衡字符串的条件。那么,对于平衡字符串,所有左括号后面都会跟着一个相应的右括号。
现在,我们可以**通过重复地将每对匹配的括号()减少为空字符串来检查这一点。**此外,当替换命令无法匹配模式时,我们将停止。并且,如果我们留下空行,则原始字符串是平衡的:
6.4. 缓冲区管理
到目前为止,我们已经对文件执行了各种编辑操作。尽管我们验证了更改在屏幕上可见,但猜猜看,**我们的更改并未永久写入磁盘上的文件中。这是因为我们正在使用缓冲区,它实际上是文件内容的副本并驻留在易失性内存中。**就此而言,甚至寄存器也是缓冲区的一类。
要将更改永久写入磁盘上的文件,我们可以使用w(写入)或 w! (强制写入) 命令:
:w[!] [filename]
我们必须注意,我们可以通过提供与当前打开的文件不同的文件名来将更改写入新文件。而且,稍后,如果我们想退出,那么我们可以使用q ( quit ) 或 *q! (退出而不写)*命令。然而,一旦我们退出 vi,我们也会丢失其寄存器中的所有可用数据。
让我们选择包含 HTTP 和 HTTPS URI 的隔离列表的文件。现在,我们的要求是我们应该将这些隔离列表放在两个不同的文件中,即http_urls.txt和https_urls.txt。
要使用多个缓冲区,我们将使用*edit (e)*命令,以便我们可以借助 vi 的命名寄存器跨文件传输文本:
:e [file1 file2 ...]
因此,vi 要求我们在切换到另一个文件进行编辑之前保存当前文件。然而,有时,我们可能不想保存更改;在这种情况下,我们可以使用e! 恢复到文件缓冲区的最后写入版本。
7. .exrc环境文件
我们每个人都有独特的工作风格,因此,我们当然喜欢编辑器中的一些个性化设置。对于 vi,执行此操作的方法是通过.exrc文件。
让我们探讨一下如何调整 vi 编辑会话。
7.1. set命令
顾名思义,**set命令帮助我们设置控制编辑器功能行为的标志和变量的值。**嗯,我们中的一些人通常喜欢在左侧看到行号。
因此,我们可以根据需要使用更多此类标志。为此,我们可以 在ex模式下使用set命令查看所有可用的选项:
:set all
7.2. 缩写
为了节省一些击键次数,vi 让我们在*.exrc*文件中定义缩写:
ab <short-phrase> <expanded-text>
**设置缩写词后,我们可以通过在输入缩写词后按空格键在插入模式下使用它。**但是,它仅在作为整个单词使用时扩展,而不是作为另一个单词的一部分使用时扩展。
稍后,如果我们想删除缩写,那么我们可以使用unab命令:
:unab <phrase>
7.3. 键映射
vi 的另一个有用功能是可以使用map命令定义自定义键盘快捷键,从而更快地完成操作:
map <NewKeySequence> <TargetKeySequence>
假设我们的工作需要处理大量以逗号分隔的数据。**那么,如果我们可以通过使用单个按键从列数据生成逗号分隔值,**不是很好吗?好吧,让我们继续将此行为分配给V键:
map V :1,$-1s/$/,/^M :%j^M
我们必须注意,^M代表回车键击的文本表示,需要将其以<Ctrl-v><CR>
键的组合形式写入.exrc文件中。