如何使用大多数 Debian/Ubuntu 机器上安装的基本工具来递归搜索和替换目录中的多个文件?
Stack* 中有多个答案,可以在其中找到该问题的答案,例如这里或者这里。但所有这些都在本质上有所欠缺。除了可能输入的一些“简单”子集之外,他们不提供正确的解决方案。
经过对 、和的手册页进行一些搜索和仔细研究后grep
,这是我能够为其构建的最好的“搜索和替换”命令xargs
sed
重击:
grep -ErlIZ -- '<OldPattern>' . | xargs -0rL1 sed -ri 's/<OldPattern>/<NewPattern>/g'
(请注意,我希望能够尽可能使用有用且高级的 shell 功能,所以我不太担心然而关于 POSIX 或可移植性——我也不太关心 Mac 中大多数过时的 GNU 工具版本)
这一单行有多种特点:
- 为了安全起见,明确忽略二进制文件(但不确定这是否真的需要)
- 用于
grep | xargs
过滤掉候选文件并在巨大的目录中提供良好的性能 - 接受以破折号 (
-
)开头的模式 - 接受带空格的路径
- 接受搜索模式中的正则表达式捕获组
但由于sed
功能集的缺陷,正则表达式引擎始终贪婪的并且没有选项可以禁用此行为(只有丑陋的解决方法)。这意味着每行只能进行一次替换,至少在某些情况下如此(如果需要,我可以显示一些示例)。
诉诸循环while
使其根据需要运行任意多次真的涵盖所有可能的替代:
while FILES="$(grep -ErlI -- '<OldPattern>' .)"; do
echo "$FILES" | xargs -rL1 sed -ri 's/<OldPattern>/<NewPattern>/g'
done
但现在Bash 无法存储空字节,因此必须删除选项grep -Z
和。我认为这会降低与包含空格的路径的兼容性。xargs -0
是否可以将
while
循环解决方案与-Z
,-0
选项结合起来以支持带空格的路径?或者也许......还有其他不同但更好的方法来构建一个强壮的和可靠的搜索和替换命令? (简洁是一个特点,因此尽可能接近一句台词)
编辑:添加一个示例,其中贪婪的正则表达式sed
对于非循环版本来说是一个问题。
使用此输入行:
set(requires "gstreamer-1.5 gstreamer-base-1.5 gstreamer-sdp-1.5 libjsonrpc")
该模式(gst.*)1\.5
将与此匹配:
set(requires "[gstreamer-1.5 gstreamer-base-1.5 gstreamer-sdp-1.5] libjsonrpc")
因为它是贪心的,所以它获取从第一个gst
到最后一个的1.5
。假设替换是\1AAA
:\1
将保留(捕获组),并且将AAA
仅打印这些字母而不是原始字母1.5
。结果将是:
set(requires "gstreamer-1.5 gstreamer-base-1.5 gstreamer-sdp-AAA libjsonrpc")
因此,该命令总共需要运行 3 次才能真正替换该行中所有可能的匹配项。循环while
版本只是一次又一次地运行所有内容,直到不再找到搜索模式,此时替换工作已完成实际上已经完成了。
答案1
如果您想一次又一次地运行替换,只要它成功,可以sed
使用以下命令通过条件循环来完成t
:
grep -ErlIZ -- '<OldPattern>' . |
xargs -r0 sed -Ei -e :1 -e 's/<OldPattern>/<NewPattern>/g' -e t1
为了提高效率,这里还传递尽可能多的文件,sed
而不是sed
每个文件运行一个文件,并且使用它比GNU 系统外部-E
更便携(并且与 一致)。-r
grep -E
bash
无法在其变量中存储 NUL,但您可以使用数组来存储文件列表。
对于 bash 4.4+:
readarray -td '' files < <(grep -ErlIZ -- '<OldPattern>' .)
然后你可以输出:
((${#files[@])) && printf '%s\0' "${files[@]}" | xargs -r0 ...
或者使用临时文件。在 Linux 上,可以这样做:
exec 3<<EOF # creates a deleted empty temp file opened on fd 3
EOF
grep -ErlIZ -- '<OldPattern>' . > /dev/fd/3 || exit
# and later:
while xargs -r0a /dev/fd/3 ...; do...
exec 3<&- # file was already deleted, closing it means its data is now
# reclaimed.
您(gst.*)1\.5
可能应该是:例如,(\<gst[^[:space:]]*)-1\.5\>
如果您希望变量部分不包含空白字符并且不匹配。tagst-1.11.51
在该示例中使用非贪婪运算符可能没有多大帮助。类似 perl 的gst.*?1.5
仍然会gstreamer-1.3 foobar-1.5
匹配set(requires "gstreamer-1.3 foobar-1.5 gstreamer-sdp-AAA libjsonrpc")