Vim 修炼场
CHAPTER 08 · 进阶篇 中

查找与替换:单文件内的批量重构

预计阅读 35 分钟(含练习约 1.5 小时)· 前置:第 2、5、7 章 · 本章演示 7 个

学习目标

  • 把 / ? n N 用成光标移动:查找不是弹窗,是跳跃
  • 掌握 * #:光标下的词一键全文件点名,并串起 * + n + . 的重命名流水线
  • 解剖 :s 命令:range + s + 模式 + 替换 + 标志五段式,g c i e 四面旗帜
  • 学会确认模式(y n a q l)与区域替换(选区 + :'<,'>s)
  • 入门 :g / :v:按内容挑行执行任意命令,:g/pattern/d 一条命令删一群行
  • 建立「文本替换 vs IDE 语义重命名(F2)」的安全边界意识

1. 查找是移动,替换是批量

先摆正两个心智模型。查找是移动:按 / 输入词回车,光标跳到下一个匹配 —— 本质上它和 w、f; 一样,是「去某个地方」的命令,只是目的地由内容定义。第 2、3 章的移动管「形状上的位置」(词首、行尾、配对括号),查找管「内容上的位置」。替换是批量动词::s 系列命令把「改」这件事从一次一处,升级到一行、一个区域、整个文件 —— 它是第 5 章动词的「批量发货」通道。

本章的地图很简单:先学「找」(/ 与 *),再学「换」(:s 与标志),然后把两者缝起来(* + n + . 工作流、:g 命令),最后划清它与 IDE 原生查找替换(F2 重命名)的边界。一个提醒先行:本章所有操作都只动当前文件 —— 跨文件的事交给 IDE 的全局搜索替换或重构工具,那是第 12 章的领地。

先用一个真实任务预演全章:函数里有个变量 cnt 要改名 count,它出现 8 次,其中 1 次在注释里。你要做的判断依次是:①要不要动注释?要 —— 一起换;不要 —— 逐处跳过。②形式统一吗?都是小写、整词 —— * 巡一圈确认。③换法:* + n + . 手动 8 处(顺眼),或 :%s/cnt/count/gc 确认模式(带保险)。你看,「找」提供情报,「换」执行决心 —— 本章每一节都在给这两个动作添加装备。

2. / 与 n/N:搜索即跳转

/ 打开查找命令行(状态栏),输入词、回车,光标落到下一个匹配处。之后 n(next)跳下一个、N 跳上一个,到文件尾会绕回文件头 —— 匹配构成一个环。? 是反向的 /(从当前往上找),日常使用率不到一成,知道即可:

三个让查找好用一倍的配置项(写在 IDE 备注里的设置里):增量查找(incsearch,边打边高亮、边跳)、高亮所有匹配(hlsearch,全文匹配常亮)。hlsearch 开了以后会带来一个小烦恼:换完事情高亮还挂着 —— :noh(no highlight)临时关掉本次高亮,值得绑到一个快捷键上。还有两个易忘的点:①查找词是正则 —— . 是「任意字符」不是句点,要找字面 a.b 得写 a\.b(§9 陷阱展开);②大小写默认敏感,Foo 查不到 foo,想不敏感在模式后加 \c。

一个组合技:查找后的移动命令全部复用 —— 查到函数名,紧接着 % 看配对括号、n 巡回所有出现点。这就是「查找是移动」的完整含义:它跳完之后,你仍在普通模式里,手里所有武器都还在。

两个进阶小件:①查找词有历史 —— 按 / 后别打字,直接按 ↑/↓ 翻最近用过的查找词,改两个字符就能复用;②:noh 之外,很多人把 Esc 之外的某个键映射成 :noh<Cr>(配置见第 12 章),高亮烦恼一劳永逸。另外注意 / 与第 3 章导航工具的分工:查找管「按内容定位」,书签与跳转表管「按位置回访」—— 找到后的位置会进跳转表(Ctrl+o 能跳回查找前的老位置),两套体系是缝合好的,不是割裂的。

3. * 与 #:光标下的词,全文件点名

高频中的高频:光标停在一个变量名上,想知道它都在哪出现。传统做法是 / + 手敲这个词 + 回车 —— 但词就在光标下,为什么要敲?*(shift+8)把光标下的词设为查找词并直接跳到下一处;# 反向跳。更妙的是 * 默认整词匹配:光标在 total 上,查的是 \<total\>,不会命中 totalPrice 里的片段 —— 这是手敲 /total 没有的待遇。

这个演示值得反复看,它是 Vim 最著名的流水线之一:* 定位 → ciw 换第一处 → n 跳下一处 → . 重复换。n 和 . 一个负责「到下一处」、一个负责「再改一遍」,交替敲击如击鼓。它与 :%s//gc(§6)是同一任务的两种解法:n + . 手感更好、顺带看语境;:%s 更快、适合无脑批量。g* 补充一句:去掉整词限定,匹配词内片段(total 命中 totalPrice)—— 改命名前先 g* 巡一遍,能看清这个词被哪些兄弟借用了。

4. :s 的解剖图:五段式与四面旗帜

替换命令的完整形状:[range]s/模式/替换/[标志]。五段拆开看:range 在哪换(§5)、s 是 substitute、模式 找什么(正则)、替换 换成什么、标志 怎么换。先看标志缺省时的行为 —— 这是全章第一大翻车点:

看清了吗:不带 g,一行只换第一处。「每行多处必带 g」请刻进肌肉。四面旗帜一览:

标志名字作用什么时候加
gglobal一行内的匹配处处换默认都加 —— 除非真想只换每行第一处
cconfirm每处动手前确认(§6)改动有语境风险、想逐处过目
iignorecase匹配忽略大小写改大小写不一致的旧代码(Foo/foo)
esilent找不到也不报错写进脚本/宏里不想被打断时(第 10 章)

替换段还有一个必会语法:& 代表「匹配到的原文」。:s/let \(\w\+\)/const \1/ 里 \1 是第一个括号组 —— 给变量统一补前缀、给字符串统一加引号这类「保留原文再加料」的替换全靠它。举例拆开看::s/\(\w\+\) = \(.*\);/\2 = \1;/ 把 a = 1; 换成 1 = a; —— 括号组把两边各自「捕获」,替换段里对调引用,一条命令完成「所有赋值语句左右互换」这种手工要命的活。正则组语法(\(...\) 与 \1)先混个脸熟,第 13 章实战会真刀真枪用。

5. range 全家:在哪换,一句话说清

range 决定 :s 的施工范围,写法是 Vim 的「行寻址」语法 —— 不止替换用,任何 : 命令都用:

写法范围典型场景
(缺省)当前行就在这行修一下
%全文件(= 1,$)批量重构的主场::%s/old/new/g
5,12第 5 到 12 行改动恰好在一个已知区间
.,$当前行到文件尾「从这里往下都换」
'<,'>可视选区选中后按 : 自动填好 —— 区域替换

区域替换是被低估的利器:同一个词在文件里出现十次,只有选区里的三处要改 —— V/v 圈地、: 自动带出 '<,'> 前缀、补上 s/old/new/g 回车,选区外毫发无伤。这比 :5,12s 数行号舒服得多 —— 范围是「圈」出来的不是「数」出来的,眼睛确认过再动手。第 7 章可视模式的动词菜单里留的那行「选中后 :」,兑现了。

range 写法还能复合微调,两个够用一生的例子:①'<,'> 出来后想多含一行,把前缀改成 '<,'>+1s/…(相对偏移);②当前行往下三行 ,+3s/…。「改哪几行」在 Vim 里永远是可精确表达的,精确到行、到选区、到「从这里到文件尾」—— 想明白这一点,就不会再出现「替换完发现多改了两行,手忙脚乱按 u」的名场面。

6. 确认模式 gc:眼皮底下的替换

:%s/old/new/gc 的 c 让替换从「批量开火」变成「逐处审批」:每处匹配,光标停上去问你换不换。五个应答键:

键意思按下去之后
yyes,换换掉这处,跳到下一处
nno,不换跳过这处,到下一处
aall,剩下的全换看过几处确认没问题,一键「都依你」
qquit,撤退立即停手(已换的不回滚)
llast,换这处就收工换掉当前处,然后结束

什么时候用 c:改动有语境风险 —— 同名词出现在注释、字符串、别的变量名里,盲换会伤及无辜;或匹配较多但只有部分要改。a 是节奏键:前三处 y 看明白了,第四处直接 a 收尾 —— 兼顾了安全与速度。注意 q 不回滚已换的,想全撤销用 u(一条 :s 是一个撤销单元)。

三种「部分替换」路线的取舍值得横向看一遍:①* + n + .:每处停顿、逐处判断,顺带看清上下文,适合改动少而精;②:s//gc 确认模式:vim 替你排队,光标自动跳到每处匹配,你只按 y/n —— 适合匹配多但要过目;③裸 :%s:全信任批量,适合你已用 * 巡视过、确认所有匹配都是同类。三档「信任度」,按你对这份代码的把握选 —— 这也是为什么查找要放在替换前面学:先侦查,后开火。

7. 重复的艺术:& 与替换历史

替换之后经常想「把同样的替换在别处再放一遍」。普通模式里的 &(shift+7)= 在当前行重复上一次 :s(标志不带回,想带 g 用 :g& 或 :&&)。配上行移动,就是逐行审批式替换:

j & j & 的节奏与 §3 的 n + . 异曲同工:一个管「到下一行」,一个管「再放一遍」。三个补充:①命令行里 ↑/↓ 翻命令历史 —— 上次的 :%s/... 可以翻出来改两个字符再用;②分隔符不必是 /::s#old/path#new/path# 用 #,替换文本里带斜杠的路径不用转义;③:~ 重复上一次替换、沿用上次的模式 —— 用得少,见到认得即可。

8. :g 与 :v:按内容挑行,执行任意命令

:g(global)是本章的深水区,也是 vim 最强大的命令之一。语法::g/模式/命令 —— 找出所有匹配的行,对每一行执行后面的命令。最经典的形态就是删行:

:g/console/d 删掉所有调试日志行、:g/^$/d 删空行、:g/^\s*\/\//d 删注释行 —— 「一类行整批消失」的需求它包圆。:v(invert)是反选:对不匹配的行执行命令,:v/^#/d = 删掉所有不以 # 开头的行(只留注释)。命令段不限于 d::g/TODO/normal A(待办) 对每个 TODO 行执行普通模式命令(normal,第 10 章展开)。两个工程事实:一条 :g 是一个撤销单元(一次 u 整条回滚,演示里演了);:g 匹配的也是正则 —— ^ 行首 $ 行尾是它的常用拼图。

日常最顺手的几条 :g 存货,按需取用:删空行 :g/^$/d、删纯空格行 :g/^\s*$/d、删注释行 :g/^\s*\/\//d(js 系)、删含某词的行 :g/word/d、把匹配行搬到文件尾 :g/pattern/m$(m 是 move,:g 家族里仅次于 d 的常用命令)、只保留匹配行 :v/pattern/d。这些命令的共性是「以行为单位做筛选」—— 当你的意图能描述成「某类行如何如何」时,先想 :g,再想正则,最后才考虑肉眼逐行。

9. 意图→工具决策表 + 安全边界

本章加上 IDE,你手里有一排查找替换工具。按意图选:

意图首选理由
看一个词都在哪出现、挨个跳过去* + n零输入,整词匹配
去某个内容的位置(一次性)/ 词 Enter任意正则,增量高亮
换掉本行/选区/全文件的一段文本:s 家族范围 + 标志精确控制
多处出现、部分要改部分不要:s//gc 或 n+.逐处过目,人为把关
一类行整批删除/加工:g/模式/d按内容挑行,一条命令一群行
真正的重命名(变量/函数/类)IDE 的 Rename(F2/Shift+F6)语义级:跨文件、懂作用域、不误伤注释字符串

最后一行是安全边界,单独强调::s 是文本替换,它分不清「代码里的引用」和「注释里恰好一样的词」;IDE 的 Rename 是语义替换,按作用域解析、自动跨文件、还会提示重名冲突。重命名一个局部变量,多数时候 :%s 没问题;但凡它是个导出函数、公共类 —— 请用 Rename,一处不漏还不动注释。反过来,Rename 干不了的活也很多:改字符串内容、改注释措辞、改日志格式 —— 这些「非语义」的文本变换正是 :s 的主场。实践派的折中流程:先 * 巡一遍看所有出现点的语境 → 数量少就 n + . 手动 → 数量多且全部同类就 :%s → 拿不准语义就 F2。三个工具不是竞争关系,是按「改动性质」分工的同事。

10. 每日训练计划(30 分钟 × 3 天)

天时间盒内容通过标准
第 1 天0–10 分钟重放演示 1–2,读 §1–§3;IDE 里开好 incsearch/hlsearch;靶场 * 巡游 10 个词* 与 n 不假思索
10–30 分钟练习 1 + 练习 2 前半n + . 流水线成型
第 2 天0–10 分钟重放演示 3–5,读 §4–§5;把 :s 五段式抄进便签说出 % 与 '<,'> 的含义
10–30 分钟练习 2 后半 + 练习 3g 标志不再漏
第 3 天0–10 分钟重放演示 6–7,读 §6–§9;确认模式五键表背一遍y/n/a/q/l 秒答
10–30 分钟练习 4:盲测毕业考10 题对 7 题,本章毕业

11. 如果本章只记住三件事

  1. 查找是移动:/ 词 Enter 跳过去,n/N 环形巡游;* 零输入点名光标词 —— * + n + . 是重命名的鼓点;
  2. :s 五段式、一行多处必带 g:[range]s/模式/替换/[标志];选区 '<,'> 圈着换,确认 c 逐处审批(y n a q l);
  3. :g 按内容挑行::g/console/d、:g/^$/d 一条命令一群行;真正的重命名交给 IDE 的 F2 —— 文本替换别越权。

查找替换解决了「批量改」,下一章解决「复制的东西去哪了」:寄存器 —— 48 个抽屉的剪贴板系统,以及与系统剪贴板的打通。

练习

练习 1:* 巡游与 / 精投(10 分钟)

靶场任选文件:①光标放 10 个不同标识符上,各做一遍 * + 连按 n 巡完全部出现点,再用 N 倒着走一遍;②再用 / 手输词找 5 次,体会增量高亮;③找一个驼峰词,分别 *(整词)与 g*(片段)各巡一遍,看清命中范围差异。通过线:三次以内 n 就能判断「这个词一共出现在几类地方」。

练习 2:重命名流水线双打(10 分钟)

造一个出现 ≥6 次的变量:①上半场用 * + ciw + Esc + n . 手动换完全部;②u 撤到起点,下半场用 :%s/旧/新/gc 确认模式再换一遍,前两处 y,确认安全后 a 收尾。体会两种节奏的手感差。通过线:两条路都不中断、不误伤。

练习 3:range 与标志实验田(10 分钟)

一段 ≥20 行、含重复词的代码:①当前行 :s/x/y/ 与 :s/x/y/g 对比;②% 全换再看 u 回滚;③V 选 5 行 → : → 区域替换,验证选区外无伤;④.,$s/x/y/g 从光标换到尾;⑤故意写 :s/a.b/c/ 遇一次正则点号,再用 :s/a\.b/c/ 修正。通过线:五个实验的结果都能提前说对。

练习 4:盲测毕业考(10 分钟)

10 个任务先说按键再看答案:①跳到下一个 foo ②光标词全文件找 ③反着找上一个匹配 ④只换当前行第一处 foo ⑤全文件把 foo 换 bar 且逐处确认 ⑥确认模式里「剩下全换」按什么 ⑦只在选区里替换 ⑧删掉所有空行 ⑨删掉所有含 TODO 的行 ⑩把上一条 :s 在当前行再来一遍。答案:/foo Enter / * / N / :s/foo/bar/ / :%s/foo/bar/gc / a / 选中后 :'<,'>s/foo/bar/g / :g/^$/d / :g/TODO/d / &。10 对 7 毕业。

常见陷阱

🕳 :%s 不带 g,以为全换了 :%s/foo/bar/ 只换每一行的第一处 —— 行内第二处开始的 foo 全都安然无恙,而你看着终端滚过的输出以为大功告成。自查口诀:「一行多处必带 g」。反过来,当你真想只动每行第一处(比如给每行首个逗号后加内容)时,不带 g 恰好是正解 —— 两种意图都想清楚再敲。
🕳 正则元字符没转义 模式段是正则:. 匹配任意字符、* 是量词、[ ] ^ $ 都有含义。想按字面匹配 a.b.js 要写 a\.b\.js;嫌转义烦可以整段「降魔」:模式前加 \V(very nomagic)后,除了 \ 全按字面算。替换段里 & 和 ~ 也有特殊含义(代表匹配原文),想按字面用要写 \&。
🕳 确认模式里手滑 a,换过头了 a(all)之后的每一处都不再询问 —— 手一快,注释里、字符串里不该换的也换掉了。两个保险:①a 之前至少 y 过两三处、看清匹配的都是同类;②真换坏了别慌,一条 :s 是一个撤销单元,普通模式一个 u 整条回滚重来。中途想停用 q(注意它不回滚已换的)。
🕳 替换文本里有 /,命令被截胡 :s/const old/const new/path/ —— 分隔符是 /,替换文本里的路径把它搅乱了。解法:分隔符随意换,:s#const old#const new/path# 用 # 一了百了。这是 vim 命令行的通用设计:: 命令里 s 后跟什么字符,什么字符就是分隔符。

VSCode / IDEA 差异

VSCode(VSCodeVim)

  • VSCodeVim 的 / 用自绘高亮模拟;对应设置 vim.incsearch(默认开)与 vim.hlsearch(建议开,高亮所有匹配)。:noh 可用
  • */# VSCodeVim 支持(整词语义一致);n/N 会复用 VSCode 的查找高亮,观感与原生一致
  • 原生对照组:Cmd+f 查找、Cmd+r 替换控件(带正则开关)、F2 语义重命名 —— 三者与 vim 方式并存,按 §9 决策表选用
  • vscode 多光标替换(Cmd-d 后直接打字)是第四条路:散落的相同词各改各的,与 *+. 二选一

IntelliJ IDEA(IdeaVim)

  • IdeaVim 的 / 查找接入 IDEA 的高亮;建议 .ideavimrc 里写 set hlsearch incsearch(第 12 章给出推荐配置全文)
  • */#/n/N IdeaVim 支持;:%s//gc 确认模式同样可用(交互提示由 IdeaVim 自绘)
  • 原生对照组:Cmd+f/Cmd+r 查找替换、Shift+F6 语义重命名(Refactor 菜单)—— 重命名公共符号优先它
  • IDEA 的 Find in Files(Cmd+Shift+f)管跨文件搜索,vim 的 :s 管当前文件 —— 边界记住「单文件用 vim,跨文件用 IDE」

本章速记

按键 / 命令作用按键 / 命令作用
/ 词 Enter跳到下一个匹配n / N下一个 / 上一个匹配
* / #光标词整词查找(反 / 正向)g*光标词片段匹配
:s/a/b/ → …/g本行第一处 → 行内处处:%s/a/b/g全文件替换
…/gc + y n a q l逐处确认的五个应答:'<,'>s/…选区内替换(选中按 :)
:g/pat/d / :v/pat/d删匹配行 / 删非匹配行&当前行重复上条 :s
:noh关掉本次查找高亮F2 / Shift+F6IDE 语义重命名(安全牌)

一句话心法:查找是移动(* 零输入点名),替换五段式(一行多处必带 g),按内容挑行用 :g,真重命名交给 F2。