Linux-06 vim 生存指南:模式化编辑的设计意图与十几个键的高效改配置
这一篇的目标不是让你放弃 VS Code 改用 vim,而是解决一个现实问题:登录一台生产服务器,你只有 vi。
服务器上没有图形界面、没有 VS Code Remote、精简镜像里连 nano 都可能没装,但 vi 一定在(它是 POSIX 要求的)。改一行 nginx 配置、调一个 systemd 参数、临时注释掉一段代码——这些事你必须能在 30 秒内做完。
先回答五个问题:
- vim 为什么要分「模式」?这不是给自己找麻烦吗?
- 为什么是
hjkl而不是方向键? - 打开文件后按了几下键,现在满屏乱七八糟,怎么撤销并原样退出?
:w时报E45: 'readonly' option is set,但我明明是 root,怎么办?- 打开文件时提示
swap file already exists,该选哪个?
1. 为什么是模式化编辑
1.1 一个历史约束造就的设计
现在看 vim 的模式切换很反直觉,但把它放回 1976 年就完全合理了:
vi 的作者 Bill Joy 当时用的是 ADM-3A 终端,通过 300 波特的调制解调器连接——每秒只能传约 30 个字符,屏幕刷新一整屏要 10 秒以上。
在这种条件下,「所见即所得」的编辑器是不可能的:你不能指望每敲一个字符就重绘屏幕。必须让每一次按键都携带尽可能多的信息,用最少的字节完成最多的编辑。
于是有了两个关键设计:
① 分模式,让每个字母键都能当命令用。 如果编辑器只有一种模式(像记事本),那 26 个字母全部被「输入文本」占用,命令只能靠 Ctrl、Alt 组合键。而 vi 把状态分开后,普通模式下 d、w、y、p 这些单键就都是命令了——一次按键 = 一条命令,这在 30 字符/秒的线路上是决定性的优势。
② 动作 + 对象的组合语法。 不是记住几百个快捷键,而是记住十几个「动作」和十几个「对象」,然后自由组合(第 3 章详述)。
Ctrl-Alt-Shift 组合键(emacs 风格) vs 单键命令(vi 风格)
删除一个单词:M-d dw
删除三行: C-u 3 C-k 3dd
这个设计在今天依然有价值的原因是:你的手不用离开主键盘区,也不用按住修饰键。但代价就是那道著名的学习陡坡。
1.2 hjkl 的来源
开篇第二问。ADM-3A 终端的键盘上,h、j、k、l 这四个键上直接印着方向箭头——因为那个年代的终端没有独立方向键,Ctrl-H/J/K/L 就是光标控制字符。Bill Joy 直接沿用了键盘上已有的标记。
ADM-3A 键盘上的印刷
h j k l
← ↓ ↑ →
今天为什么还留着? 因为方向键在主键盘区之外,移动手掌去够它会打断输入节奏。hjkl 在标准手位上,右手食指到小指恰好落在这四个键。用熟之后确实更快——这也是为什么 Gmail、GitHub、Jira、tmux、less、man 都支持 j/k 上下滚动,它已经成了跨软件的通用约定。
一个记忆技巧:j 的字形像一个向下的钩子(↓),k 像向上的旗杆(↑),h 在左边(←),l 在右边(→)。
2. 五分钟生存:先保证不出事
如果你只有五分钟,记住这一节就够改配置了。
2.1 三个模式
i / a / o / I / A / O
+----------------------------------------+
| v
[ Normal ] [ Insert ]
普通模式:敲键 = 执行命令 <---- Esc ---- 插入模式:敲键 = 输入文字
| ^
| |
| Esc / 回车
v |
[ Command-line ]
命令行模式:以 : 开头,用于保存、退出、替换
输入 : / ? 进入
核心规则:不确定自己在什么模式时,多按几下 Esc 就能回到普通模式。 这是最重要的一条。
2.2 最小可用命令集
vim /etc/nginx/nginx.conf
# ── 进入编辑 ──
i # insert,在光标【前】插入
a # append,在光标【后】插入
o # open,在下面开一个新行并进入插入模式 ← 加配置行最常用
Esc # 回到普通模式
# ── 移动(普通模式)──
hjkl # ← ↓ ↑ →
gg # 跳到文件第一行
G # 跳到最后一行
42G # 跳到第 42 行 ← 配合报错信息里的行号
0 # 行首
$ # 行尾
/pattern # 向下搜索,回车确认,n 下一个 N 上一个
# ── 编辑(普通模式)──
x # 删除光标处的一个字符
dd # 删除(剪切)当前行 ← 最常用
3dd # 删除 3 行
yy # 复制当前行
p # 粘贴到下一行
u # 撤销 ← 救命键
Ctrl-R # 重做
# ── 保存退出(命令行模式)──
:w # write,保存
:q # quit,退出
:wq # 保存并退出(等价 :x 或普通模式下按 ZZ)
:q! # 【不保存】强制退出 ← 改坏了就用它
2.3 改坏了怎么全身而退
开篇第三问。三个层次的后悔药:
u # 撤销一步,可以连按(vim 默认能撤销 1000 步)
Ctrl-R # 撤销撤错了,重做
U # 撤销对【当前行】的所有修改(大写 U,一次性还原整行)
:e! # 放弃所有改动,重新从磁盘读取文件(相当于"重新打开")
:q! # 直接退出不保存 ← 最保险
建议:任何时候不确定,先 Esc 再 :q!,重新打开就是。 只要没执行 :w,磁盘上的文件就是安全的。
2.4 意外情况处理
# ① 按了 Ctrl-S,屏幕完全没反应
# 原因:Ctrl-S 是终端的"流控制"(XOFF),暂停了输出,不是 vim 的问题
Ctrl-Q # 恢复(XON)
# 预防:在 ~/.bashrc 里加 stty -ixon 彻底关掉流控制
# ② 不小心按了 Ctrl-Z,vim 消失回到了 shell
# 原因:Ctrl-Z 把 vim 挂起到后台了(不是退出!)
fg # 拿回前台 ← 别以为文件丢了又开一个,那会产生 swap 冲突
jobs # 看有几个挂起的任务
# ③ 进入了「录制宏」状态(左下角显示 recording @q)
q # 再按一次 q 停止录制(是按错 q 进去的)
# ④ 进入了可视块模式或其他奇怪状态
Esc Esc Esc # 多按几次
# ⑤ 屏幕显示错乱(内容和实际不符)
Ctrl-L # 重绘屏幕
3. 动作 + 对象:vim 真正的语法
理解这一章,vim 就从「背几百个快捷键」变成「记十几个词根然后自由组合」。
3.1 语法结构
[次数] 动作 [次数] 对象/范围
3 d 2 w
└───────┴──────┴───────┘
"删除 3×2=6 个单词"
常见形式:
d w 删除到下一个单词开头
3 d d 删除 3 行
c i " 修改双引号【里面】的内容
y a p 复制一整个段落(含前后空行)
d t , 删除到下一个逗号【之前】
3.2 动作(operator)
| 键 | 全称 | 作用 |
|---|---|---|
d |
delete | 删除(剪切到寄存器) |
c |
change | 删除并进入插入模式(改写用它,省一步) |
y |
yank | 复制 |
p / P |
put | 粘贴到光标后 / 前 |
> / < |
— | 增加 / 减少缩进 |
= |
— | 自动格式化缩进 |
gu / gU |
— | 转小写 / 大写 |
! |
— | 把内容送给外部命令处理 |
动作重复两次 = 作用于整行:dd 删整行、yy 复制整行、>> 整行缩进。
3.3 移动(motion):既能单独用,也能当对象
| 键 | 含义 | 组合示例 |
|---|---|---|
w / W |
下一个词首(W 以空白分隔) |
dw 删一个词 |
e / E |
词尾 | de |
b / B |
上一个词首(back) | db |
0 |
行首(第 0 列) | d0 删到行首 |
^ |
行首第一个非空白字符 | d^ |
$ |
行尾 | d$ 删到行尾(等价 D) |
gg / G |
文件首 / 尾 | dG 删到文件末尾 |
{ / } |
上/下一个段落(空行分隔) | d} |
% |
跳到配对的括号(()、[]、{}) |
d% 删除整个括号块 |
f<char> |
find:跳到本行下一个该字符 | df, 删到逗号(含逗号) |
t<char> |
till:跳到该字符之前 | dt, 删到逗号前(不含) |
F / T |
同上,但向左找 | dF" |
; / , |
重复上一次 f/t 的查找 / 反向重复 |
|
/pattern |
搜索也是一种移动 | d/END 删到 END 处 |
H / M / L |
屏幕的顶 / 中 / 底 | High/Middle/Low |
3.4 文本对象(text object):最提效的部分
格式是 i(inner,内部)或 a(around,含边界)加上对象类型:
| 对象 | 含义 | 例子(光标在括号/引号内任意位置) |
|---|---|---|
iw / aw |
单词 | diw 删一个词,daw 连带空格一起删 |
i" / a" |
双引号内 / 含引号 | ci" 改写引号里的内容 |
i' / a' |
单引号 | di' |
i` |
反引号 | |
i( i) ib |
圆括号内 | ci( 改函数参数 |
i{ i} iB |
花括号内 | di{ 删除整个代码块 |
i[ / i] |
方括号 | |
i< / i> |
尖括号 | |
it / at |
HTML/XML 标签内 / 含标签 | cit 改标签内容 |
ip / ap |
段落 | dap 删整段 |
is / as |
句子 |
这是 vim 最值得学的部分,因为它不需要精确定位光标:
# 光标在这一行任意位置: server_name example.com;
ci" # 如果值带引号,直接改引号内的内容
ct; # change till ; —— 改到分号之前
# 光标在这行任意位置: proxy_pass http://127.0.0.1:3000;
f: # 跳到第一个冒号
ct; # 改到分号前
# 光标在函数体内任意位置
di{ # 清空整个 {} 里的内容
ci{ # 清空并开始输入
# 改一个 JSON 值(光标在 value 上任意位置)
ci" # "old_value" -> 直接输入新值
3.5 高效组合速查
# ── 改写类(用 c 而不是 d + i,省一步)──
cw # 改一个词
ciw # 改光标所在的整个词(不管光标在词中间哪里) ← 比 cw 好用
ci" ci' # 改引号内容
ci( ci{ # 改括号内容
C # = c$,改到行尾
cc 或 S # 改整行(清空并进入插入)
# ── 删除类 ──
diw # 删一个词
di" # 删引号内容
D # = d$,删到行尾
dG # 删到文件末尾
dgg # 删到文件开头
d/foo # 删到下一个 foo 处
# ── 复制粘贴 ──
yy / Y # 复制整行
yiw # 复制一个词
y$ # 复制到行尾
p / P # 粘贴到下方/上方(行复制时)或光标后/前(词复制时)
yyp # 【复制当前行并粘贴】—— 复制一行配置再改,最高频操作
ddp # 【当前行与下一行交换】
# ── 缩进 ──
>> << # 当前行增减缩进
3>> # 3 行
>i{ # 缩进整个代码块
=G # 从当前行到文件尾自动缩进
# ── 大小写与递增 ──
~ # 切换光标处字符大小写
guiw / gUiw # 一个词转小写 / 大写
Ctrl-A # 光标处的数字 +1 ← 改端口号很方便
Ctrl-X # 数字 -1
# ── 重复 ──
. # 【重复上一次修改操作】← vim 最强大的键,没有之一
. 的威力举个例子:
# 需求:把配置里所有的 8080 改成 9090(只改前几个)
/8080 # 搜索
cw9090 Esc # 改第一个
n # 跳到下一个
. # 重复"改成 9090"这个动作
n . n . # 继续
# 比 :%s 更可控,因为你能逐个确认
4. 实战:改配置文件的完整流程
假设 nginx 启动报错 nginx: [emerg] invalid port in "8O80" of the "listen" directive in /etc/nginx/conf.d/app.conf:12。
sudo vim +12 /etc/nginx/conf.d/app.conf
# ^^^ 直接跳到第 12 行(也可以打开后按 12G)
# 屏幕上是: listen 8O80;
# ^ 字母 O 不是数字 0
f8 # 跳到第一个 8
ciw # 改这个词
8080 # 输入正确的值
Esc # 回普通模式
:w # 保存
:q # 退出
# 或者 ZZ 一次搞定(保存并退出)
sudo nginx -t && sudo systemctl reload nginx
再看几个高频场景:
# ── 场景一:注释掉一段配置(第 20~30 行)──
:20,30s/^/#/ # 在 20~30 行的行首插入 #
# 或者用可视块模式(更直观):
20G # 跳到 20 行
Ctrl-V # 进入【可视块】模式
10j # 向下选 10 行
Shift-I # 在块的左侧插入
# # 输入 #
Esc # ← 按下后所有选中行都会加上 #(有延迟,属正常)
# 取消注释
:20,30s/^#//
# 或可视块选中那一列的 # 后按 x
# ── 场景二:批量替换 ──
:%s/old/new/g # 全文替换(% = 全文,g = 每行所有匹配)
:%s/old/new/gc # 加 c = confirm,逐个确认 ← 生产环境推荐
:10,20s/old/new/g # 只在 10~20 行
:%s/old/new/gi # i = 忽略大小写
:%s#/usr/local#/opt#g # 内容带 / 时换个分隔符,避免转义
:g/DEBUG/d # 删除所有含 DEBUG 的行
:v/ERROR/d # 删除所有【不含】ERROR 的行(v = invert)
# ── 场景三:在文件末尾追加配置 ──
G # 跳到最后一行
o # 下面开新行
# 输入内容
Esc :wq
# ── 场景四:从另一个文件导入内容 ──
:r /path/other.conf # read,把另一个文件的内容插入到光标下方
:r !date # 把命令的【输出】插入进来
:r !curl -s ifconfig.me
# ── 场景五:把选中内容交给外部命令处理 ──
:%!sort # 全文排序
:%!jq . # 全文 JSON 格式化 ← 很实用
:%!python3 -m json.tool
:10,20!sort -u # 只处理 10~20 行
5. 那些让人卡住的坑
5.1 只读文件:用 tee 提权保存
开篇第四问。
vim /etc/hosts # 忘了加 sudo
# 改了半天...
:w
# E45: 'readonly' option is set (add ! to override)
:w!
# E212: Can't open file for writing <- 强制也不行,因为是权限问题
# ✅ 神级解法:借 sudo tee 之手写入
:w !sudo tee %
# ^ 注意 w 和 ! 之间有空格
# 含义:把缓冲区内容通过管道送给 `sudo tee 当前文件名`
# % 会被展开成当前文件的路径
# 执行后会提示 "W12: Warning: File changed... [O]K, (L)oad File:",按 L 重新加载
# 更方便的做法:写进 ~/.vimrc 定义一个命令
# command! W w !sudo tee % > /dev/null
# 之后直接 :W 即可
为什么这招管用? vim 进程本身是普通用户权限,改不了 /etc/hosts。但 :w !cmd 是把缓冲区内容通过管道传给一个新起的命令,而这个命令可以用 sudo 提权。tee 读取标准输入并写入文件,正好胜任。
5.2 swap 文件冲突
开篇第五问。
vim /etc/nginx/nginx.conf
# E325: ATTENTION
# Found a swap file by the name ".nginx.conf.swp"
# owned by: root dated: Wed Aug 12 21:00:00 2026
# file name: /etc/nginx/nginx.conf
# modified: YES <- 关键信息!
# process ID: 12345 (still running) <- 关键信息!
#
# Swap file ".nginx.conf.swp" already exists!
# [O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:
怎么选?先看那两行关键信息:
| 情况 | 含义 | 选什么 |
|---|---|---|
process ID: xxx (still running) |
另一个 vim 真的开着这个文件(可能是你 Ctrl-Z 挂起的那个,或同事在改) |
选 Q 退出,然后 fg 找回原来那个;或联系同事 |
modified: YES 且进程不在了 |
vim 崩溃/断线了,swap 里有未保存的修改 | 选 R 恢复,比对后 :w 保存,再手动删 swap |
modified: no 且进程不在了 |
只是残留,没有未保存内容 | 选 D 删掉 |
# 恢复流程(选 R 之后)
# 1. vim 从 swap 里恢复了内容
:w # 确认无误后保存
:q
rm /etc/nginx/.nginx.conf.swp # 手动删掉 swap,否则下次还提示
# 命令行方式恢复
vim -r /etc/nginx/nginx.conf # 直接从 swap 恢复
vim -r # 列出所有能恢复的 swap 文件
# 找出系统里所有残留的 swap 文件
find / -name '.*.sw[a-p]' 2>/dev/null
swap 文件是什么? vim 在编辑时会把改动持续写入一个 .文件名.swp,用途有二:① 支持编辑超大文件(不用全部载入内存);② 崩溃后能恢复。它是特性不是 bug。
" 如果你不想要 swap 文件(比如在 /etc 里编辑会留下垃圾)
:set noswapfile
" 或者统一放到一个目录,不污染工作目录
" 在 ~/.vimrc 里:set directory=~/.vim/swap//
5.3 粘贴时缩进阶梯式增长
# 现象:从浏览器复制一段 YAML 粘进 vim,缩进变成
# a:
# b:
# c:
# d:
# 原因:vim 的自动缩进把"粘贴进来的空格"当成了"你敲的空格",叠加了
# ✅ 解法一:粘贴前进入 paste 模式
:set paste
# 粘贴
:set nopaste # 记得关掉,否则 vimrc 里的自动缩进都失效
# ✅ 解法二:绑定快捷键(写进 .vimrc)
" set pastetoggle=<F2> 按 F2 切换
# ✅ 解法三(最好):vim 8.0+ 的终端支持「括号粘贴」,多数情况自动处理
# 如果没生效,检查 :set paste? 和终端是否支持 bracketed paste
5.4 中文乱码
:set encoding=utf-8 " vim 内部使用的编码
:set fileencoding=utf-8 " 当前文件保存时用的编码
:set fileencodings=utf-8,gbk,gb18030,latin1 " 打开文件时【依次尝试】的编码
" 临时用指定编码重新打开
:e ++enc=gbk " 用 GBK 重新读取当前文件
:w ++enc=utf-8 " 用 UTF-8 保存
" 查看当前文件的编码
:set fileencoding?
:set fileformat? " unix(LF) / dos(CRLF) / mac(CR)
:set fileformat=unix " 把 CRLF 改成 LF 后保存
5.5 其他常见卡点
# ① :q 时报 E37: No write since last change
:q! # 放弃修改退出
:wq # 或者保存后退出
# ② 打开了多个文件,:q 只关了一个
:qa # quit all
:wqa # 保存所有并退出
:qa! # 全部强制退出
# ③ 搜索后满屏都是高亮,很干扰
:noh # nohlsearch,清除高亮(下次搜索仍会高亮)
# ④ 想看行号
:set nu # number
:set nonu # 关闭
:set rnu # relativenumber,相对行号(配合 5j / 3dd 很方便)
# ⑤ 误删了一大段,撤销也回不去(撤销超限)
:earlier 10m # 回到 10 分钟前的状态!(vim 的时间旅行)
:later 5m # 前进 5 分钟
:undolist # 看撤销树
# ⑥ 编辑巨大文件(几百 MB)卡顿
vim -u NONE huge.log # 不加载任何配置和插件,速度最快
# 更好:大文件根本不该用编辑器打开,用 less / sed / awk
6. 提效功能
6.1 多文件与窗口
vim a.conf b.conf # 打开多个文件
:n # next,下一个文件
:prev # 上一个
:ls # 列出所有缓冲区
:b 2 # 切到 2 号缓冲区
:b name # 按名字切(支持部分匹配)
# 分屏
:sp file # split,水平分屏
:vs file # vsplit,垂直分屏 ← 对比两个配置文件时好用
Ctrl-W 然后 hjkl # 在窗口间移动
Ctrl-W 然后 +/-/</> # 调整窗口大小
Ctrl-W 然后 = # 窗口大小均分
Ctrl-W 然后 q # 关闭当前窗口
:only # 只保留当前窗口
# 标签页
:tabe file # 新标签打开
gt / gT # 下一个 / 上一个标签
:tabc # 关闭标签
6.2 寄存器(剪贴板)
"ayy # 复制当前行到寄存器 a
"ap # 粘贴寄存器 a 的内容
:reg # 查看所有寄存器内容
"+y # 复制到【系统剪贴板】(需要 vim 编译时带 +clipboard)
"+p # 从系统剪贴板粘贴
vim --version | grep clipboard # 检查是否支持,显示 -clipboard 表示不支持
# 服务器上通常不支持系统剪贴板,用终端自己的复制(鼠标选中)即可,
# 但要先 :set mouse= 关掉 vim 的鼠标接管,否则选中的是 vim 的可视模式
6.3 宏:重复性编辑的杀手锏
qa # 开始录制,存到寄存器 a(左下角显示 recording @a)
# ... 执行一系列操作 ...
q # 停止录制
@a # 回放一次
10@a # 回放 10 次
@@ # 重复上一次回放
# 实战:给每行末尾加分号
qa # 开始录制
A; # 跳到行尾追加分号
Esc # 退出插入
j # 下移一行
q # 停止录制
100@a # 对后面 100 行执行同样操作
6.4 一个够用的最小 vimrc
服务器上不建议装插件(每台机器都要配、影响启动速度),但十几行配置能显著提升体验:
" ~/.vimrc —— 最小实用配置
set nocompatible " 关闭 vi 兼容模式(必须放第一行)
syntax on " 语法高亮
set number " 显示行号
set ruler " 右下角显示光标位置
set showcmd " 右下角显示正在输入的命令
set expandtab " Tab 转成空格(YAML/Python 必须)
set tabstop=4 " Tab 显示为 4 个空格宽
set shiftwidth=4 " 缩进命令(>>)使用 4 空格
set softtabstop=4
set autoindent " 新行保持上一行的缩进
set hlsearch " 搜索结果高亮
set incsearch " 边输入边搜索
set ignorecase " 搜索忽略大小写
set smartcase " 但搜索词含大写时区分大小写
set encoding=utf-8
set fileencodings=utf-8,gbk,gb18030,latin1
set backspace=indent,eol,start " 让退格键能正常删除(老 vi 的坑)
set mouse= " 关闭鼠标接管,让终端的复制粘贴正常工作
set laststatus=2 " 总是显示状态栏
" 让 :W 能用 sudo 保存只读文件(见 5.1)
command! W w !sudo tee % > /dev/null
" 用 jk 快速退出插入模式(可选,但很多人喜欢)
" inoremap jk <Esc>
" 记住上次编辑的位置
autocmd BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal! g`\"" | endif
# 临时用一份干净配置(排查是不是配置导致的问题)
vim -u NONE file # 完全不加载配置
vim -u ~/minimal.vim file # 指定配置文件
7. vi、vim、nvim 与 nano
# 服务器上 vi 可能是这三种之一,先确认
readlink -f "$(command -v vi)"
# /usr/bin/vim.basic <- Ubuntu:vi 指向 vim 的精简版
# /usr/bin/vim.tiny <- 更精简(没有语法高亮、没有多级撤销!)
# /usr/bin/busybox <- Alpine:busybox 内置的极简 vi
vim --version | head -2
# VIM - Vi IMproved 8.2 (2019 Dec 12, compiled ...)
| 说明 | |
|---|---|
vi |
POSIX 标准要求的编辑器,任何 Unix 系统一定有。功能极简 |
vim |
Vi IMproved,最常用。多级撤销、语法高亮、插件、可视模式 |
vim.tiny |
Debian/Ubuntu 最小安装时的默认 vi,没有多级撤销(u 只能撤一步,再按是重做) |
nvim |
Neovim,vim 的现代分支。默认配置更合理、Lua 配置、内置 LSP |
nano |
完全不同的思路:无模式,底部常驻快捷键提示(^X 表示 Ctrl-X)。改一行配置最快上手 |
如果只是改一行配置且系统里有 nano,用 nano 也完全合理——工具是为了解决问题,不是为了显示水平:
nano /etc/hosts
# Ctrl-O 保存(WriteOut) Enter 确认文件名
# Ctrl-X 退出
# Ctrl-W 搜索(Where is)
# Ctrl-K 剪切当前行
# Ctrl-\ 替换
但 vi 是唯一保证存在的,所以第 2 章那套最小命令集仍然值得记住。
# 设置默认编辑器(影响 visudo、git commit、crontab -e)
export EDITOR=vim
export VISUAL=vim
sudo update-alternatives --config editor # Debian 系的系统级设置
8. 知识点扩展
8.1 命令行启动参数
| 参数 | 作用 |
|---|---|
vim +N file |
打开后跳到第 N 行 |
vim + file |
打开后跳到最后一行 |
vim +/pattern file |
打开后搜索并跳到第一个匹配 |
vim -R file |
只读模式(等价 view) |
vim -M file |
强制不可修改 |
vim -r file |
从 swap 恢复 |
vim -r |
列出所有可恢复的 swap |
vim -d a b |
diff 模式(等价 vimdiff) |
vim -o a b |
水平分屏打开多个文件 |
vim -O a b |
垂直分屏打开 |
vim -p a b |
用标签页打开 |
vim -u NONE file |
不加载任何配置(排查问题、编辑大文件) |
vim -es +'%s/a/b/g' +wq file |
脚本模式批处理(无界面) |
vim -c 'cmd' file |
打开后执行指定的 ex 命令 |
vim -b file |
二进制模式(配合 :%!xxd) |
# 在脚本里用 vim 批量改文件(其实更推荐 sed,但偶尔有用)
vim -es +'%s/8080/9090/g' +wq config.conf
# 用 vim 看/改二进制
vim -b file
:%!xxd # 转成十六进制显示
# 编辑...
:%!xxd -r # 转回二进制
:wq
8.2 常用 ex 命令(: 开头)
| 命令 | 作用 |
|---|---|
:w / :w file |
保存 / 另存为 |
:w !sudo tee % |
用 sudo 保存只读文件 |
:q / :q! / :qa! |
退出 / 强制 / 全部强制 |
:wq / :x / ZZ |
保存并退出(:x 仅在有改动时才写) |
:e file |
打开另一个文件 |
:e! |
放弃改动重新加载 |
:r file / :r !cmd |
插入文件内容 / 命令输出 |
:!cmd |
执行外部命令 |
:%!cmd |
把全文交给外部命令处理(:%!jq .、:%!sort) |
:N,Ms/a/b/g |
在 N~M 行替换 |
:%s/a/b/gc |
全文替换并逐个确认 |
:g/pat/d |
删除所有匹配行 |
:v/pat/d |
删除所有不匹配的行 |
:g/pat/normal @a |
对所有匹配行执行宏 a |
:sort / :sort u |
排序 / 排序去重 |
:set opt? |
查看某个选项的当前值 |
:noh |
清除搜索高亮 |
:earlier 10m |
回到 10 分钟前的状态 |
:help keyword |
查帮助(:h dd) |
8.3 常用 set 选项
| 选项 | 作用 |
|---|---|
nu / rnu |
行号 / 相对行号 |
paste / nopaste |
粘贴模式(防缩进阶梯) |
list |
显示不可见字符(tab、行尾) |
wrap / nowrap |
长行折行 / 不折行 |
ic / noic |
ignorecase |
hls / nohls |
搜索高亮 |
expandtab / noexpandtab |
Tab 转空格 |
ts=4 sw=4 sts=4 |
tabstop / shiftwidth / softtabstop |
ff=unix |
文件格式(改 CRLF 为 LF) |
fenc=utf-8 |
文件编码 |
noswapfile |
不产生 swap |
mouse= |
关闭鼠标接管 |
readonly! |
切换只读标志 |
8.4 完整快捷键速查
# ══ 模式 ══
i I a A o O 插入:光标前/行首/光标后/行尾/下方新行/上方新行
v V Ctrl-V 可视模式:字符/行/块
R 替换模式(覆盖输入)
Esc 回到普通模式
# ══ 移动 ══
h j k l ← ↓ ↑ →
w b e 词首/上一词/词尾
0 ^ $ 行首/首个非空/行尾
gg G 42G 文件首/文件尾/第42行
{ } 上/下一段
Ctrl-F Ctrl-B 下/上一页
Ctrl-D Ctrl-U 下/上半页
H M L 屏幕顶/中/底
% 跳到配对括号
f t F T ; , 行内查找字符
* # 搜索光标下的单词(向下/向上)
'' `. 回到上次位置 / 上次修改处
Ctrl-O Ctrl-I 跳转历史后退/前进
# ══ 编辑 ══
x X 删除光标处/前一个字符
dd yy p P 删行/复制行/粘贴到下方/上方
D C 删到行尾/改到行尾
J 合并下一行到本行
r<char> 替换单个字符
u Ctrl-R 撤销/重做
. 重复上次修改
~ 切换大小写
Ctrl-A Ctrl-X 数字加一/减一
# ══ 可视模式下 ══
d y c 删除/复制/修改选中内容
> < 缩进/反缩进
u U 转小写/大写
:'<,'>s/a/b/g 只在选中范围替换
9. 面试题
Q:vim 为什么要设计成模式化编辑?
这是 1976 年的硬件约束逼出来的设计。vi 的作者 Bill Joy 用的是 300 波特的连接——每秒约 30 个字符,刷新一整屏要十几秒。在这种条件下,编辑器必须让每次按键携带最大信息量。
分模式的直接收益是:普通模式下 26 个字母键全部可以当命令用(d 删除、y 复制、p 粘贴),一次按键就是一条命令;而无模式编辑器(记事本、nano)的字母键都被输入占用,命令只能靠 Ctrl/Alt 组合。
今天这个设计仍有价值的原因变了:手不离开主键盘区、不用按修饰键,配合「动作 + 对象」的组合语法,可以用很少的记忆量表达很多操作。hjkl 同理——ADM-3A 终端的键盘上这四个键就印着方向箭头,沿用至今是因为它们在标准手位上,而这个约定后来扩散到了 less、man、tmux、Gmail、GitHub。
Q:dw、diw、daw、cw 有什么区别?vim 的命令语法是什么结构?
vim 的语法是 [次数] 动作 [次数] 对象:
dw= delete word,从光标当前位置删到下一个词首。光标在词中间时只删后半截。diw= delete inner word,删除光标所在的整个词(不管光标在词的哪个位置),不含周围空格。daw= delete around word,删整个词并连带一侧空格。cw= change word,删除后直接进入插入模式(比dw然后i少一步)。
真正提效的是文本对象(i/a + 对象类型):ci" 改引号内的内容、di{ 删整个代码块、ci( 改函数参数——它们的共同优势是不需要精确定位光标,在目标区域内任意位置都能用。
另外必须记住 .(重复上次修改),配合 n 可以做到「搜索 → 改第一个 → n . n . 逐个确认着改」,比 :%s 更可控。
Q:用 vim 打开了 /etc/hosts 忘记加 sudo,改完发现存不了,怎么办?
:w 报 E45: 'readonly',:w! 报 E212: Can't open file for writing——因为这是文件系统权限问题,不是 vim 的只读标志问题,强制也没用。
解法是 :w !sudo tee %(注意 w 和 ! 之间有空格)。原理::w !cmd 把缓冲区内容通过管道送给一个新起的外部命令,这个命令可以用 sudo 提权;tee 读标准输入并写入文件,% 展开成当前文件路径。执行后会提示文件已变更,按 L 重新加载即可。
可以在 .vimrc 里定义 command! W w !sudo tee % > /dev/null,之后直接 :W。
Q:打开文件提示 swap file already exists,Recover / Delete / Quit 该选哪个?
先看提示里的两行关键信息:
process ID: xxx (still running)—— 说明另一个 vim 真的开着这个文件(很可能是你自己按Ctrl-Z挂起的那个)。应该选Q退出,然后fg找回原来那个进程。这时候选E(Edit anyway)会导致两个 vim 同时改一个文件,后保存的覆盖先保存的。modified: YES且进程已不存在 —— vim 崩溃或 ssh 断线了,swap 里存着未保存的修改。选R恢复,确认内容无误后:w保存,再手动rm掉 swap 文件(否则下次还提示)。modified: no且进程不在 —— 只是残留,选D删掉。
swap 文件本身是特性不是 bug:它让 vim 能编辑超过内存的大文件,也提供崩溃恢复能力。命令行恢复用 vim -r file,列出所有可恢复的用 vim -r。
Q:从网页复制一段 YAML 粘进 vim,缩进变成阶梯状递增,为什么?
因为 vim 的 autoindent/smartindent 把「粘贴进来的空格」当成了「你手敲的空格」——每一行它都会先按上一行自动缩进,然后再叠加上原文自带的缩进,于是层层累加。
解法:粘贴前 :set paste(临时关闭所有自动缩进和自动补全),粘完 :set nopaste。可以用 set pastetoggle=<F2> 绑到一个键上。
vim 8.0+ 在支持 bracketed paste 的终端上会自动处理这个问题(终端会给粘贴内容加上特殊标记,vim 据此区分「粘贴」和「输入」)。如果还遇到,说明终端或 TERM 设置不支持。
Q:服务器上的 vi 和 vim 是一回事吗?
不一定。vi 通常是个软链接,readlink -f "$(command -v vi)" 能看到真身:
vim.basic(Ubuntu 常见)—— 功能基本完整vim.tiny(Debian/Ubuntu 最小安装)—— 没有多级撤销(u只能撤一步,再按变成重做)、没有语法高亮,用起来很容易出事busybox(Alpine 容器)—— busybox 内置的极简 vi,命令集非常有限
所以在陌生机器上先确认一下。真正做保证的是 vi 一定存在(POSIX 要求),所以最小命令集(i/Esc/:wq/:q!/dd/u//搜索)必须记牢。
如果系统里有 nano,改一行配置用 nano 完全合理——它无模式、底部有常驻快捷键提示(^X 就是 Ctrl-X),上手成本几乎为零。工具是为了解决问题。
Q:在 vim 里怎么快速把一段 JSON 格式化、把配置排序?
用 :%!外部命令 —— 把整个缓冲区通过管道交给外部程序处理,再用输出替换缓冲区内容:
:%!jq . JSON 格式化
:%!python3 -m json.tool 同上(没装 jq 时)
:%!sort 全文排序
:%!sort -u 排序去重
:10,20!sort 只处理 10~20 行
:%!xxd / :%!xxd -r 二进制与十六进制互转
这体现了 vim 的一个设计哲学:不重复造轮子,把文本处理交给 Unix 工具链。同理 :r !cmd 能把命令输出插入到光标下方(:r !date、:r !curl -s ifconfig.me)。
Q:按了 Ctrl-S 之后 vim 完全没反应,是崩溃了吗?
没崩溃。Ctrl-S 是终端的流控制字符(XOFF),它暂停了终端的输出,和 vim 没有关系——你的按键其实都被接收了,只是屏幕不刷新。按 Ctrl-Q(XON) 就能恢复,之前敲的内容会一次性显示出来(这也是为什么恢复后经常发现文件被改乱了,用 u 撤销即可)。
预防:在 ~/.bashrc 里加 stty -ixon 彻底关掉流控制,这样 Ctrl-S 就可以被程序自由使用了。
同类的还有 Ctrl-Z:它不是退出,而是把 vim 挂起到后台(发 SIGTSTP)。用 fg 就能拿回来。很多人以为文件丢了又开一个 vim,结果撞上 swap 文件冲突——这两个坑是连着的。
小结
- vim 的模式设计源于 300 波特终端的约束:让每次按键携带最大信息量。今天的价值是手不离主键盘区 + 组合语法
- 五分钟生存集:
i插入、Esc回普通、:wq保存退出、:q!后悔药、dd删行、u撤销、/搜索、42G跳行 - 真正的提效来自
[次数] 动作 [次数] 对象的组合语法,尤其是文本对象:ci"、di{、ciw不需要精确定位光标 .是最强的键,配合n能逐个确认地批量修改- 只读文件用
:w !sudo tee %;swap 冲突先看process ID和modified两行再决定 Ctrl-S是终端流控不是崩溃(Ctrl-Q恢复),Ctrl-Z是挂起不是退出(fg找回):%!cmd把文本交给 Unix 工具链处理(:%!jq .、:%!sort)- 服务器上先
readlink -f $(command -v vi)确认真身,vim.tiny没有多级撤销
下一篇讲 find 与 xargs:find 的参数不是「选项」而是表达式,理解它的求值模型才能明白为什么 -o 和 -delete 混用会误删;以及 -exec {} \; 与 {} + 差几个数量级的性能、-print0 | xargs -0 为什么是唯一安全的管道写法。
xingliuhua