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

Aku*_*eru 5 performance find shell-script files

我想以最有效的方式递归更改数百个文件的第一行。我想要做的一个例子是更改#!/bin/bash#!/bin/sh,所以我想出了这个命令:

find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;
Run Code Online (Sandbox Code Playgroud)

但是,据我所知,这样做 sed 必须读取整个文件并替换原始文件。有没有更有效的方法来做到这一点?

ilk*_*chu 19

是的,完全sed -i读取和重写文件,并且由于行长度发生变化,因此必须移动所有其他行的位置。

...但在这种情况下,行长实际上不需要改变。我们可以#!/bin/sh??用两个尾随空格代替hashbang 行。操作系统将在解析 hashbang 行时删除那些。(或者,使用两个换行符或一个换行符 + 哈希符号,这两者都会创建 shell 最终会忽略的额外行。)

我们需要做的就是从一开始就打开文件进行写入,而不是截断它。通常的重定向>>>不能这样做,但在 Bash 中,读写重定向<>似乎有效:

echo '#!/bin/sh  ' 1<> foo.sh
Run Code Online (Sandbox Code Playgroud)

或使用dd(这些应该是标准的 POSIX 选项):

echo '#!/bin/sh  ' | dd of=foo.sh conv=notrunc
Run Code Online (Sandbox Code Playgroud)

请注意,严格来说,这两个都重写了行尾的换行符,但这并不重要。

当然,上面的内容会无条件地覆盖给定文件的开头。添加一个检查,原来的文件已经在正确的hashbang留作练习......无论如何,我可能不会在生产中做到这一点,很显然,如果你需要将线更改为这是不行的一个.

  • 通常,最好将现有数据提取到第一个换行符,并制作一个新版本(相同长度)以进行覆盖。可以在 shell 前插入两个空格,如 `#! /bin/sh`,以保留以下任何选项。事实上,只要对格式有足够的了解,您就可以找到足够多的非重要空间来插入一些文本并仍然原地更新。无论如何,系统将读取/更改/写入至少一个完整的块,这提供了足够的范围。 (3认同)

Qua*_*odo 7

优化将使用{} +而不是{} \;.

find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +
Run Code Online (Sandbox Code Playgroud)

不是为每个找到的文件调用一个 sed 进程,而是将文件作为参数提供给单个 sed 进程。

find 的 POSIX 规范{} +(我的粗体):

如果主表达式由 <加号> 标点,则主表达式应始终评估为真,并且评估主表达式的路径名应聚合为集合。实用程序utility_name 应为每组聚合路径名调用一次。


Sté*_*las 5

我会做:

#! /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
Run Code Online (Sandbox Code Playgroud)

就像@ilkkachu 的方法一样,文件被一个大小完全相同的字符串覆盖就地。区别在于:

  • 我们忽略隐藏文件和隐藏目录中的文件(.git例如考虑一个),因为您不太可能考虑那些(您使用的find ./*会跳过当前目录的隐藏文件和目录,而不是子目录的隐藏文件和目录)。D如果确实需要,请添加glob 限定符。
  • 我们不会费心查看那些不足以容纳要替换的原始 shebang 的文件(我们使用.相当于-type f,因此我们已经从文件中检索 inode 信息,因此我们不妨检查那里的大小)。
  • 我们实际上是在检查文件是否以要替换的正确shebang 开头,根据需要读取尽可能少的字节(此处必须如此,zsh因为其他shell 无法处理任意字节值)。
  • 我们正在使用#!/bin/sh -作为替代,这是/bin/sh脚本的正确shebang (顺便说一下,这#!/bin/bash -将是脚本的正确shebang /bin/bash)。请参阅为什么“#!/bin/sh -”shebang 中的“-”?详情。

覆盖文件的错误在退出状态中报告,但不是遍历目录树的错误,也不是读取文件的错误,尽管可以添加。

在anycase,它只替换了shebangs准确 #!/bin/bash,没有其他shebangs在使用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
Run Code Online (Sandbox Code Playgroud)

这里允许许多不同的 shebangs 和许多受支持的选项,这些选项在新的/bin/shshebang中复制,右填充(使用r[length]参数扩展标志)与原始大小相同。