目录

Linux-06 vim 生存指南:模式化编辑的设计意图与十几个键的高效改配置

这一篇的目标不是让你放弃 VS Code 改用 vim,而是解决一个现实问题:登录一台生产服务器,你只有 vi。

服务器上没有图形界面、没有 VS Code Remote、精简镜像里连 nano 都可能没装,但 vi 一定在(它是 POSIX 要求的)。改一行 nginx 配置、调一个 systemd 参数、临时注释掉一段代码——这些事你必须能在 30 秒内做完。

先回答五个问题:

  1. vim 为什么要分「模式」?这不是给自己找麻烦吗?
  2. 为什么是 hjkl 而不是方向键?
  3. 打开文件后按了几下键,现在满屏乱七八糟,怎么撤销并原样退出?
  4. :w 时报 E45: 'readonly' option is set,但我明明是 root,怎么办?
  5. 打开文件时提示 swap file already exists,该选哪个?

1. 为什么是模式化编辑

1.1 一个历史约束造就的设计

现在看 vim 的模式切换很反直觉,但把它放回 1976 年就完全合理了:

vi 的作者 Bill Joy 当时用的是 ADM-3A 终端,通过 300 波特的调制解调器连接——每秒只能传约 30 个字符,屏幕刷新一整屏要 10 秒以上

在这种条件下,「所见即所得」的编辑器是不可能的:你不能指望每敲一个字符就重绘屏幕。必须让每一次按键都携带尽可能多的信息,用最少的字节完成最多的编辑。

于是有了两个关键设计:

① 分模式,让每个字母键都能当命令用。 如果编辑器只有一种模式(像记事本),那 26 个字母全部被「输入文本」占用,命令只能靠 CtrlAlt 组合键。而 vi 把状态分开后,普通模式下 dwyp 这些单键就都是命令了——一次按键 = 一条命令,这在 30 字符/秒的线路上是决定性的优势。

② 动作 + 对象的组合语法。 不是记住几百个快捷键,而是记住十几个「动作」和十几个「对象」,然后自由组合(第 3 章详述)。

Ctrl-Alt-Shift 组合键(emacs 风格)  vs  单键命令(vi 风格)
删除一个单词:M-d                        dw
删除三行:    C-u 3 C-k                  3dd

这个设计在今天依然有价值的原因是:你的手不用离开主键盘区,也不用按住修饰键。但代价就是那道著名的学习陡坡。

1.2 hjkl 的来源

开篇第二问。ADM-3A 终端的键盘上,hjkl 这四个键上直接印着方向箭头——因为那个年代的终端没有独立方向键,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):最提效的部分

格式是 iinner,内部)或 aaround,含边界)加上对象类型:

对象 含义 例子(光标在括号/引号内任意位置)
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:dwdiwdawcw 有什么区别?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,改完发现存不了,怎么办?

:wE45: '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:服务器上的 vivim 是一回事吗?

不一定。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 IDmodified 两行再决定
  • Ctrl-S 是终端流控不是崩溃Ctrl-Q 恢复),Ctrl-Z 是挂起不是退出fg 找回)
  • :%!cmd 把文本交给 Unix 工具链处理(:%!jq .:%!sort
  • 服务器上先 readlink -f $(command -v vi) 确认真身,vim.tiny 没有多级撤销

下一篇讲 find 与 xargsfind 的参数不是「选项」而是表达式,理解它的求值模型才能明白为什么 -o-delete 混用会误删;以及 -exec {} \;{} + 差几个数量级的性能、-print0 | xargs -0 为什么是唯一安全的管道写法。