查找与替换:单文件内的批量重构
学习目标
- 把 / ? 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」请刻进肌肉。四面旗帜一览:
| 标志 | 名字 | 作用 | 什么时候加 |
|---|---|---|---|
| g | global | 一行内的匹配处处换 | 默认都加 —— 除非真想只换每行第一处 |
| c | confirm | 每处动手前确认(§6) | 改动有语境风险、想逐处过目 |
| i | ignorecase | 匹配忽略大小写 | 改大小写不一致的旧代码(Foo/foo) |
| e | silent | 找不到也不报错 | 写进脚本/宏里不想被打断时(第 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 让替换从「批量开火」变成「逐处审批」:每处匹配,光标停上去问你换不换。五个应答键:
| 键 | 意思 | 按下去之后 |
|---|---|---|
| y | yes,换 | 换掉这处,跳到下一处 |
| n | no,不换 | 跳过这处,到下一处 |
| a | all,剩下的全换 | 看过几处确认没问题,一键「都依你」 |
| q | quit,撤退 | 立即停手(已换的不回滚) |
| l | last,换这处就收工 | 换掉当前处,然后结束 |
什么时候用 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 后半 + 练习 3 | g 标志不再漏 | |
| 第 3 天 | 0–10 分钟 | 重放演示 6–7,读 §6–§9;确认模式五键表背一遍 | y/n/a/q/l 秒答 |
| 10–30 分钟 | 练习 4:盲测毕业考 | 10 题对 7 题,本章毕业 |
11. 如果本章只记住三件事
- 查找是移动:/ 词 Enter 跳过去,n/N 环形巡游;* 零输入点名光标词 —— * + n + . 是重命名的鼓点;
- :s 五段式、一行多处必带 g:
[range]s/模式/替换/[标志];选区'<,'>圈着换,确认 c 逐处审批(y n a q l); - :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/foo/bar/ 只换每一行的第一处 —— 行内第二处开始的 foo 全都安然无恙,而你看着终端滚过的输出以为大功告成。自查口诀:「一行多处必带 g」。反过来,当你真想只动每行第一处(比如给每行首个逗号后加内容)时,不带 g 恰好是正解 —— 两种意图都想清楚再敲。
. 匹配任意字符、* 是量词、[ ] ^ $ 都有含义。想按字面匹配 a.b.js 要写 a\.b\.js;嫌转义烦可以整段「降魔」:模式前加 \V(very nomagic)后,除了 \ 全按字面算。替换段里 & 和 ~ 也有特殊含义(代表匹配原文),想按字面用要写 \&。
: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+F6 | IDE 语义重命名(安全牌) |
一句话心法:查找是移动(* 零输入点名),替换五段式(一行多处必带 g),按内容挑行用 :g,真重命名交给 F2。