vi 最让人困惑的地方,不是命令多,而是同一个按键会随状态改变含义。你在 Insert 模式按 j,得到的是字母;回到 Normal 模式再按 j,光标向下移动。只要先建立“状态 + 动作 + 作用范围”的模型,后面的命令就不再是一堆零散快捷键。
这一章以可移植的 vi 基线为主,同时把常见 Vim 扩展单独标出。你会完成真实编辑、搜索替换、另存与恢复实验,也会知道什么时候该停下来检查,而不是直接敲 ! 强行通过。
vi 到底启动了什么vi 是一个命令名,不是一份跨系统完全相同的功能清单。它可能指向 Vim、Debian 的 vim-tiny、nvi,也可能是 BusyBox 提供的精简 applet。基础移动和编辑通常相似,帮助系统、多级撤销、Visual 选区、窗口管理等能力却可能不同。
先从 Shell 侧看路径和链接:
command -v vi
readlink -f "$(command -v vi)"
ls -l "$(command -v vi)"进入编辑器后,再执行:
:version
:set compatible?
:help有些程序通过环境变量选择编辑器:
printf '%s\n' "$EDITOR" "$VISUAL"
EDITOR=vi VISUAL=vi some-commandEDITOR=vi 或 VISUAL=vi 只表达“希望调用名为 vi 的编辑器”,仍然没有说明这个名字最终指向哪种实现。程序也不一定同时读取两个变量;具体优先级要看调用方说明。排查 Git、crontab 或邮件工具打开了意外编辑器时,需要同时检查变量、alternatives 和符号链接。
如果 :help 报错,不代表 vi 坏了;精简实现可能没有完整帮助文件。Debian Bookworm 的 vim-tiny 就以较小体积提供 /usr/bin/vi,编译时会裁掉 eval、语法高亮等功能。遇到教程里的命令不可用时,第一反应应是核对实现和功能,而不是不断重复同一个按键。

下文标为“Vim 扩展”的功能不要默认写进必须跨系统运行的操作手册。最稳妥的基线是先掌握 POSIX vi 的模式、移动、操作符、搜索和 Ex 命令,再按实际实现添加功能。
启动后通常位于 Normal 模式。此时按键是命令:h 移动、x 删除、i 切换模式。按 i、a、o 等命令后进入 Insert,普通字符才会写入编辑缓冲区。按 Esc 回到 Normal,再按 : 打开 Command-line,输入 :w、:q、:s 之类的 Ex 命令。
Vim 还常用 v、V、Ctrl-V 进入字符、行、块 Visual 选区。这是很实用的 Vim 能力,但不要和 POSIX 规范里“vi 的 visual 屏幕编辑状态”混成同一件事,更不能假定每个 vi 都有高亮选区。

编辑器先把文件内容读进 buffer。移动、删除、替换和撤销都发生在 buffer 里;只有写入命令成功后,磁盘文件才改变。状态栏里的 [Modified]、[+] 或相似标记表示 buffer 和最近一次写入状态已经不同。
你可以把流程看成三步:
磁盘文件 --读取--> buffer --编辑--> 已修改 buffer --:w--> 磁盘文件这能解释两个常见现象:u 可以撤销还在 buffer 里的修改;:q 会阻止你丢掉未写入内容。它也解释了为什么 :q! 危险:感叹号不是“更可靠地保存”,而是明确放弃保护。

普通编辑使用:
vi config.conf只想查看时,可以使用 view 或 vi -R:
view config.conf
vi -R config.conf它们会设置 readonly 选项,降低误写概率,但不是安全边界。Vim 可能允许 :w! 越过编辑器内部提示,而内核仍会检查文件权限、目录权限和只读挂载。换句话说,! 不能提升权限,也不能让只读文件系统变成可写。
重要配置建议先做三项检查:
ls -l config.conf
readlink config.conf || true
stat config.conf如果路径是符号链接,先确认目标;如果只是审阅,保留 -R;如果需要实验,复制到隔离目录再改。对于未知来源的文件,vim -Nu NONE -n file 可以跳过用户配置和 swap,但 -n 也意味着这次会话没有 swap 恢复保障,应按目标选择,不要机械套用。
屏幕看起来相同的文本,字节格式也可能不同。Unix 文本通常用 LF 结束一行,来自 Windows 的文件常用 CRLF。Vim 可以检测并尽量保持换行格式,可用下面的命令核对:
:set fileformat?
:set fileformats?精简 vi 未必提供同样的自动检测选项。如果版本控制突然显示“整份文件都被修改”,先检查行尾和编码,不要立刻做全局替换掩盖问题。
插入命令的区别只在“从哪里开始输入”:
输入结束后按 Esc 返回 Normal。Esc 在终端协议中也是控制序列的前缀,因此高延迟连接、tmux 配置或终端快捷键可能让它显得“慢半拍”。先看模式提示,再按一次 Esc;不要因为没有立刻看到变化就连续乱敲命令。

最小闭环是:
i → 输入文本 → Esc → :w → 回车 → :q → 回车h、j、k、l 分别向左、下、上、右移动。它们的价值不只是让手留在主键区,更重要的是能直接充当操作符的 motion。
常用移动可以按尺度记:
Vim 常见的 gg 跳到首行很方便,但不是所有 vi 的可移植保证。需要跨实现时用 1G 或 Ex 地址 :1 更稳妥。Ctrl-F/B/D/U 的滚动量还会受窗口大小和选项影响,适合“快速到附近”,不适合写成精确行数算法。
word 也不只是“肉眼看到的一串字符”。字母、数字和下划线通常组成一类 word,标点会形成另一类;空白负责分隔。大写 W、B、E 则把连续非空白字符当成更大的 WORD。编辑 key=value 时,w 会在标点附近停下,W 往往直接跨过整段。删除前先移动一次观察落点,可以避免把 motion 的边界想错。

operator + motion + count 是 vi 的核心语法d 删除、c 更改、y 复制,它们都等待一个 motion 来确定范围:
dw 删除到下一个 word 边界
d$ 删除到行尾
cw 更改到下一个 word 边界,并进入 Insert
y$ 复制到行尾这套组合比背 dw、d$、cw、y$ 四个孤立快捷键更有扩展性。学会新的 motion,就等于同时扩展了多个操作符。
motion 还有“是否包含终点”的细节。d$ 包含行尾字符,dw 通常把下一个 word 前的空白一起处理,de 则停在当前 word 末尾。不要仅凭命令名字猜结果:先在副本里用同一个 motion 移动一次,确认光标终点;Vim 环境也可以用 Visual 选区观察范围,再回到 operator 语法。

2dw 把计数交给操作符,d2w 把计数交给 motion;在这个例子里都删除两个 word。两边都有计数时会相乘,例如 Vim 中 2d3w 会覆盖六个 word。
重复操作符把范围提升为整行:dd 删除当前行,cc 更改当前行,yy 复制当前行。计数仍可组合:3dd 删除三行。为了让操作容易复核,实践中建议把计数写在最前面,例如 3dw、3dd,不要故意堆叠两个计数炫技。
有些编辑不需要 operator + motion,直接命令更快:
u 与 . 解决的是两个方向:u 回退,. 把同一修改应用到新位置。例如先 cwtimeoutEsc 把一个词改为 timeout,移动到另一个词再按 .,Vim 会重复“更改该范围并插入 timeout”这整次修改。
Vim 的 Ctrl-R 用来重做被 u 撤销的修改;经典 vi 与其他实现的 control-R 可能用于重绘,撤销模型也可能不同。因此跨实现操作只承诺 u,需要重做时先查当前帮助。Vim 中还要注意:撤销后再做新修改会产生新的历史分支,Ctrl-R 不一定回到你原来预想的路线。
p 和 P 放什么vi 不把删除和复制结果丢掉,而是写入寄存器。d、c、x 会把删除的文本放入寄存器,y 只复制不改 buffer。随后 p 从光标之后放入,P 从光标之前放入。
放入位置还取决于寄存器类型:
p 放到光标字符后,P 放到前。p 放到当前行下方,P 放到上方。ddp 因此常用来把当前行向下移动一行。yy 与 Y 在常见 vi/Vim 中都复制整行。不要把 Y 猜成 y$;它保留了历史上的行操作语义。命名寄存器可以避免重要复制内容被下一次删除覆盖:
"ayy 把当前行复制到寄存器 a
"ap 从寄存器 a 放入
"Ayy 追加到寄存器 a(Vim 常见行为)未命名寄存器会随下一次删除、更改或复制更新。刚用 yy 保存一行,随后又用 x 删除字符,再按 p 时得到什么,取决于实现的寄存器规则,结果可能已经不是那一整行。需要稳定复用时使用命名寄存器,并用 Vim 的 :registers a 或当前实现的显示命令核对内容。

% 都是可组合的 motion/pattern 向前搜索,?pattern 向后搜索;n 沿上次搜索方向找下一个,N 反向找。方向是相对“上次搜索”定义的,所以执行 ?error 后,n 仍然向上找,不能把 n 永远理解成“向下”。
行内精确定位常用:
f, 跳到本行下一个逗号
t, 停在本行下一个逗号之前
F, 向左跳到上一个逗号
T, 向左停在上一个逗号之后它们可以和操作符组合:dt, 删除到逗号之前,cf) 更改到右括号并进入 Insert。% 在括号上通常跳到配对的 ()、[]、{};其他匹配对象属于实现扩展。遇到嵌套结构时,先让光标落在括号上再按 %,比肉眼数括号可靠。
搜索模式使用正则表达式,不是固定字符串搜索。点号、星号、方括号等字符可能有特殊含义;不确定时先在副本里试,或查看当前实现的 pattern 帮助。
冒号命令可以在前面带地址:
常用文件命令:
:w " 写回当前文件
:q " 无未保存修改时退出
:q! " 放弃当前 buffer 的未保存修改
:wq " 写入成功后退出
:e other.conf " 用另一个文件替换当前编辑内容
:r snippet.conf " 把文件内容读入当前行之后
:w new.conf " 把当前 buffer 写到另一个路径:w new.conf 是“把内容写到新路径”,不等于把当前 buffer 一定重命名为新文件;用 :file 核对当前文件名。:e other.conf 遇到未保存修改会拒绝切换,这是防止丢失的保护。真正想重载磁盘版本并放弃修改时才使用 :e!。

替换的基本形状是:
:[range]s/pattern/replacement/[flags]例如:
:s/old/new/ " 当前行第一个匹配
:s/old/new/g " 当前行每个匹配
:%s/old/new/g " 全文件每行每个匹配
:%s/old/new/gc " 全文件替换,但逐项确认
:1,20s/old/new/g " 只处理第 1 至 20 行末尾 g 是“每一行内的全部匹配”,不是“全文件”;全文件由前面的 % 负责。c 会逐项确认,适合第一次处理重要配置。
确认模式里,Vim 常见回答包括 y 替换当前项、n 跳过、a 接受剩余匹配、q 停止。其他实现的提示可能不同,先读屏幕底部给出的选项。第一次执行大范围替换时,可以先去掉 g 或缩小到 :1,20,看结果正确后再扩大范围。
分隔符不一定非得用 /。路径中斜杠很多时可以换成 #:
:%s#/srv/old#/srv/new#gpattern 是当前实现的正则表达式。要匹配字面点号通常写 \.,要在 / 分隔形式里匹配斜杠要写 \/。不同 vi/Vim 对“扩展正则”、单词边界、大小写选项的细节不同,跨系统脚本应只用查证过的语法。
启动时给多个文件:
vi one.conf two.conf three.conf这形成参数列表。通用的 :n 前往下一个文件,:N 或 :previous 返回上一个。当前 buffer 有未保存修改时,切换会被拒绝,先 :w 或明确处理修改。
Vim 进一步区分:buffer 是载入内存的编辑对象,window 是观察某个 buffer 的视图。常见 Vim 命令包括:
:args " 查看参数列表
:buffers " 查看 buffer 列表
:buffer 2 " 切到编号 2 的 buffer
:split file " 水平窗口
:vsplit file " 垂直窗口
Ctrl-W w " 在窗口间切换这些列表彼此不等价:关闭一个 window 不一定卸载 buffer;参数列表也不等于所有 buffer。精简 vi 可能没有 buffer 列表或分屏。Visual 高亮选区、:buffers、:split 都应在帮助可用时再使用。
处理三个文件时,稳妥节奏是“改一个、:w、:n”,不要在多个 modified buffer 之间来回跳后再一次性猜哪些保存了。Vim 若启用 hidden 可以让已修改 buffer 暂时离开窗口,但这只是保留内存状态,不等于写盘;退出整个会话时仍要处理每个修改标记。
.exrc、.vimrc 与兼容模式要保持最小可解释传统 vi 使用 .exrc,Vim 常用 .vimrc。它们的每一行本质上是 Ex 命令。Vim 在 Unix 上通常按顺序寻找 ~/.vimrc、~/.vim/vimrc 或 XDG 配置路径,并只读取找到的第一份;vim -u file 可以显式指定配置,-u NONE 跳过用户配置和插件。
一个足够克制的 Vim 起点:
set nocompatible
set number
set showmatch
set noignorecasecompatible 不是单一开关,它会联动多项选项,包括撤销、搜索显示、键序列处理等。发现 u、方向键或模式提示与预期不同,先执行 :set compatible?,再查看实际加载的配置。不要复制一份几百行配置后再猜是哪一行改变了行为。
Vim 的 -V 可以把启动过程写到日志,适合追踪“谁改了这个选项”:
vim -V3/tmp/vim-startup.log target.txt
sed -n '1,160p' /tmp/vim-startup.log
rm -f /tmp/vim-startup.log日志可能包含路径、插件名和命令,分享前先检查敏感信息。排查完成后删除临时日志,避免在公共目录长期保留环境细节。
配置文件还能执行命令,来源不可信的 .exrc 或 .vimrc 存在安全风险。不要开启会自动读取当前目录配置的选项,除非理解所有权和可写权限检查。修改前留副本,新增一两项后用新会话验证:
cp ~/.vimrc ~/.vimrc.before-test
vim -u ~/.vimrc /tmp/vi-config-check.txtVim 会用 swap 保存编辑会话的恢复信息。异常退出后再次打开文件,可能看到已有 swap 的警告。常见原因有两个:另一个编辑进程仍在运行,或上一次会话崩溃留下了恢复数据。
安全顺序是:
ps、pgrep 或 lsof 确认是否仍有编辑进程。vim -r file 或进入后执行 :recover。diff -u original recovered 核对,并搜索恢复提示中的 ???。vim -r important.conf
#在编辑器内:
:w important.conf.recovered
diff -u important.conf important.conf.recovered交换文件不是备份的替代品。Vim 的 backup、writebackup、backupcopy 会改变写入策略;文件权限、ACL、扩展属性、只读挂载和符号链接也会影响结果。尤其是符号链接:某次实现保留链接并更新目标,不代表所有配置都如此。重要文件先另存、比较、再替换。
下面的实验把所有文件限制在容器的 /tmp/welearn-ch12,退出后由 --rm 删除容器。我们选 vim-tiny,是为了同时验证基础命令和精简实现边界。
docker run --rm -it \
--name welearn-linux-ch12-lab \
debian:bookworm-slim bash
apt-get update -qq
apt-get install -y -qq vim-tiny util-linux
mkdir -p /tmp/welearn-ch12
cd /tmp/welearn-ch12
printf 'one two three four\nfoo old old\n' > practice.txt
vi practice.txt进入后依次做:
gg 或 1G 到首行;若 gg 不可用就用 1G
0 到行首
2dw 删除两个 word
u 撤销
. 重复最近修改时先确认落点
/old 搜索 old
n 下一个匹配
:%s/old/new/g 全文件替换
:w copy.txt 另存
:q! 放弃当前 buffer 后退出;只在你确实要放弃时使用重新查看结果:
sed -n '1,20p' practice.txt
sed -n '1,20p' copy.txt
readlink -f "$(command -v vi)"
vim.tiny --version | sed -n '1,12p'
rm -rf /tmp/welearn-ch12
exit实测的 Bookworm 容器中,vi 指向 Vim 9.0 的 vim.tiny;2dw 与 d2w 都把 one two three four 变为 three four;dw 后按 . 也得到 three four。真实 PTY 中执行 dw → u → Ctrl-R,最终为 two three four,证明 Vim 的重做重新应用了删除。
范围实验的结果是:
call(foo) needle new new
BAR needle new
READ-IN-LINE
BAR tail needle
last new这里 %s/old/new/g 替换所有行,2,3s/foo/BAR/g 只处理第 2 至 3 行,:2read 再把 READ-IN-LINE 插入第 2 行之后。未保存时普通 :q 返回 E37,只读写入返回 E45,原文件字节保持不变。最后删除实验目录,并确认容器没有残留。
实操的重点不是“命令能跑”,而是把范围、预期文件内容、错误状态和清理都写出来。只有能解释结果,测试才真正帮助我们判断命令是否安全。
当 vi 的行为和预期不一致,按下面顺序查,比继续试快捷键更快:
1. 我在哪个模式?先按 Esc 回到 Normal。
2. vi 是哪个实现?看 command -v、readlink、:version。
3. 当前文件是谁?用 :file,检查状态栏的 [Modified]。
4. 当前命令选了什么范围?拆成 operator、motion、count 或 Ex 地址。
5. 这是 vi 基线还是 Vim 扩展?查当前版本帮助。
6. 写入失败属于编辑器提示、权限、目录、挂载还是符号链接策略?
7. 有 swap 时是否仍有进程?恢复是否已经另存并 diff?
8. 写入成功后再退出,实验目录按计划清理。vi 的速度来自组合,而可靠性来自边界意识。先知道 buffer 尚未写回,再知道命令将覆盖哪一段,最后确认实现和文件状态。这样使用 dd、:%s 或 :wq 时,你不是在碰运气,而是在执行一套可以检查、可以撤销、可以复现的操作。