我想以最有效的方式递归地更改数百个文件的第一行。我想要做的一个例子是更改#!/bin/bash
为#!/bin/sh
,所以我想出了这个命令:
find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;
但是,据我了解,这样做 sed 必须读取整个文件并替换原始文件。有没有更有效的方法来做到这一点?
答案1
是的,sed -i
完整读取并重写文件,并且由于行长度发生变化,因此必须这样做,因为它会移动所有其他行的位置。
...但在这种情况下,线长度实际上不需要改变。我们可以用#!/bin/sh␣␣
两个尾随空格替换 hashbang 行。操作系统将在解析 hashbang 行时删除这些内容。 (或者,使用两个换行符,或换行符 + 井号,这两者都会创建 shell 最终会忽略的额外行。)
我们需要做的就是打开文件从头开始写入,而不是截断它。通常的重定向>
并>>
不能做到这一点,但在 Bash 中,读写重定向<>
似乎可以工作:
echo '#!/bin/sh ' 1<> foo.sh
或使用dd
(这些应该是标准 POSIX 选项):
echo '#!/bin/sh ' | dd of=foo.sh conv=notrunc
请注意,严格来说,这两者都会重写行尾的换行符,但这并不重要。
当然,上面的内容无条件地覆盖给定文件的开头。添加对原始文件是否具有正确的 hashbang 的检查留作练习...无论如何,我可能不会在生产中执行此操作,显然,如果您需要将行更改为更长一。
答案2
一种优化是使用{} +
而不是{} \;
.
find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +
您无需为每个找到的文件调用一个 sed 进程,而是将这些文件作为参数提供给单个 sed 进程。
find on 的 POSIX 规范{} +
(我的粗体):
如果主表达式由 <加号> 标点,则主表达式应始终评估为 true,并且评估主表达式的路径名应聚合到集合中。对于每组聚合路径名,应调用一次实用程序 utility_name。
答案3
我会做:
#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
shebang_to_replace=$'#!/bin/bash\n'
new_shebang=$'#!/bin/sh -\n'
length=$#shebang_to_replace
ret=0
for file in **/*(N.L+$((length - 1)));do
if
read -u0 -k $length shebang < $file &&
[[ $shebang = $shebang_to_replace ]]
then
print -rn -- $new_shebang 1<> $file || ret=$?
fi
done
exit $ret
喜欢@ilkkachu 的方法,该文件将被大小完全相同的字符串覆盖。差异是:
- 我们忽略隐藏文件和隐藏目录中的文件(
.git
例如,考虑一个),因为您不太可能想要考虑这些文件(您使用的文件find ./*
会跳过当前目录的隐藏文件和目录,但不会跳过子目录的隐藏文件和目录)。D
如果您确实需要,请添加glob 限定符。 - 我们不会费心去查找那些不够大以容纳要替换的原始 shebang 的文件(我们使用
.
相当于-type f
,因此我们已经从文件中检索了 inode 信息,因此我们不妨检查那里的大小)。 - 我们实际上是在检查文件是否以要替换的正确 shebang 开头,并根据需要读取尽可能少的字节(这里必须如此,
zsh
因为其他 shell 无法处理任意字节值)。 - 我们使用
#!/bin/sh -
作为替换,这是脚本的正确 shebang/bin/sh
(#!/bin/bash -
将是正确的 shebang/bin/bash
顺便说一下, )。看为什么“#! /bin/sh -” shebang 中的“-”?了解详情。
覆盖文件的错误会在退出状态中报告,但不会报告遍历目录树的错误,也不会报告读取文件的错误,尽管可以添加这些错误。
无论如何,它只是取代了确切地 #!/bin/bash
,而不是其他用作解释器的 shebang,bash
如#! /bin/bash
, #! /bin/bash -Oextglob
, #! /usr/bin/env bash
, #! /bin/bash -efu
。对于这些,您需要决定要做什么。-efu
是sh
选项,但-Oextglob
没有sh
等价物。
您可以扩展它以支持最简单的情况,例如:
#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
zmodload zsh/system || exit
minlength=11 # length of "#!/bin/bash"
maxlength=1024 # arbitrary here.
ret=0
for file in **/*(N.L+$minlength);do
if
sysread -s $maxlength buf < $file &&
[[ $buf =~ $'(^#![\t ]*((/usr)?/bin/env[ \t]+bash|/bin/bash)([ \t]+-([aCefux]*))?[ \t]*)\n' ]]
then
shebang=$match[1] newshebang="#!/bin/sh -$match[5]"
print -r -- ${(r[$#shebang])newshebang} 1<> $file || ret=$?
fi
done
exit $ret
这里允许许多不同的 shebang 以及许多受支持的选项,这些选项在新的/bin/sh
shebang 中再现,右填充(带有r[length]
参数扩展标志)到与原始大小相同。
答案4
文件是一长串连续的字节。替换为bash
本质sh
上需要删除组成 的两个字节(假设是 UTF-8 或类似字节)ba
。文件中不能有漏洞,因此从开始的所有内容都sh
必须提前两个字节写入文件中。
这需要重写整个文件,或者至少从更改的部分开始。
有一些方法可以代替文件中的字节,例如,如果格式允许,则带有无辜的空格,而无需重写整个文件,请参阅接受的答案。