命令是 grep -rl 'old' . | xargs sed -i 's/old/new/g'。重要的是前半部分:grep -rl 先列出会被改动的文件,这样你能在 sed 就地重写之前先看一眼。
1. 哪些文件会被改动?
$ clai 列出 proj 下包含 old_name 的文件→ grep -rl 'old_name' proj/proj/src/b.pyproj/src/a.py
-r 递归查找,-l 打印文件名而不是匹配的行。两个文件——这是个可信的数字。如果显示 4000,说明你的模式太宽泛,正好帮你省下一下午的麻烦。
2. 只在这些文件里替换
$ clai 把 proj 下所有包含 old_name 的文件里的 old_name 替换成 new_name→ grep -rl 'old_name' proj/ | xargs sed -i 's/old_name/new_name/g'$ cat proj/src/a.pynew_name = 1
只有匹配的文件会被打开。如果直接对整个目录树跑 sed -i,会重写仓库里每个文件的修改时间,包括那些根本没变的成千上万个文件。
3. 文件名带空格也不会出问题
$ clai 同样的替换,但要能安全处理带空格的文件名→ grep -rlZ 'old_name' proj/ | xargs -0 -r sed -i 's/old_name/new_name/g'
-Z 让 grep 用空字节分隔文件名,-0 让 xargs 按空字节解析。少了这一对,my file.py 会被当成两个参数传进去,sed 一个都改不了。-r 则避免了没有匹配时 xargs 在没有参数的情况下运行 sed。
4. 跳过永远不想动的目录
$ clai 替换时跳过 node_modules、.git 和 vendor→ grep -rlZ --exclude-dir={.git,node_modules,vendor} 'old_name' . | xargs -0 -r sed -i 's/old_name/new_name/g'
不排除这些目录,迟早会改到某个依赖的源码,diff 会长达 4 万行。
5. 只处理一种文件类型
$ clai 只在 python 文件里替换→ grep -rlZ --include='*.py' 'old_name' . | xargs -0 -r sed -i 's/old_name/new_name/g'
--include 是 --exclude 的白名单版本。范围越窄越安全。
6. 保留备份——同时保持跨平台
$ clai 做替换的同时给每个文件保留备份,在 mac 上也能用→ grep -rlZ 'old_name' proj/ | xargs -0 -r sed -i.bak 's/old_name/new_name/g'
sed -i.bak 会在每个被改动的文件旁边写一个 a.py.bak。这也是唯一一种在 GNU 和 BSD 的 sed 上都能用的写法:macOS 要求带后缀,Linux 两种都接受。
常见坑
- 你的模式是正则表达式,不是普通字符串。
.、*、/和$都是特殊字符。替换config.yml也会匹配到configXyml;记得转义,或者换一个文本里不会出现的分隔符:sed -i 's|old/path|new/path|g'。 - 先提交代码。
sed -i没有撤销功能。替换前保持 git status 干净,出错时只需一句git checkout .。 grep -l每个文件只要匹配一次就停止——这正是它快的原因,也不影响替换本身,因为sed仍然会处理每一行。
相关问题
怎么先预览替换效果而不直接应用? 去掉 -i 让它直接打印:grep -rlZ 'old' . | xargs -0 sed 's/old/new/g' | less。或者用 grep -rn 'old' . 查看每一行及行号。
为什么什么都没发生? 通常是 xargs 拿到了空列表,或者你的模式没匹配到你以为的内容。单独跑一下 grep -rl 那部分看看。
能用 find 代替吗? 可以:find . -type f -name '*.py' -exec sed -i 's/old/new/g' {} +。但它会重写每个名字匹配的文件,不管里面有没有该模式,所以更推荐 grep -rl 的方案。
另请参阅
描述你想要的替换,看看 CliAI 生成的命令,再运行它。一行命令安装。