更改文件中 1 行的最有效方法

更改文件中 1 行的最有效方法

我想以最有效的方式递归地更改数百个文件的第一行。我想要做的一个例子是更改#!/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。对于这些,您需要决定要做什么。-efush选项,但-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/shshebang 中再现,右填充(带有r[length]参数扩展标志)到与原始大小相同。

答案4

文件是一长串连续的字节。替换为bash本质sh上需要删除组成 的两个字节(假设是 UTF-8 或类似字节)ba。文件中不能有漏洞,因此从开始的所有内容都sh必须提前两个字节写入文件中。

这需要重写整个文件,或者至少从更改的部分开始。

有一些方法可以代替文件中的字节,例如,如果格式允许,则带有无辜的空格,而无需重写整个文件,请参阅接受的答案。

相关内容