ファイル内の1行を変更する最も効率的な方法

ファイル内の1行を変更する最も効率的な方法

何百ものファイルの最初の行を、できるだけ効率的な方法で再帰的に変更したいと考えています。 私がやりたいことの一例として、 を変更することを挙げます。#!/bin/bashそこで#!/bin/sh、次のコマンドを考えました。

find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;

しかし、私の理解では、この方法では sed はファイル全体を読み取り、元のファイルを置き換える必要があります。これを行うより効率的な方法はありますか?

答え1

はい、sed -iファイル全体を読み取り、書き換えます。行の長さが変わるため、他のすべての行の位置を移動して、そうする必要があります。

...ただし、この場合、行の長さを実際に変更する必要はありません。#!/bin/sh␣␣代わりに、ハッシュバン行を 2 つの末尾のスペースで置き換えることができます。OS は、ハッシュバン行を解析するときにそれらを削除します。(または、2 つの改行、または改行 + ハッシュ記号を使用します。どちらも、シェルが最終的に無視する余分な行を作成します。)

必要なのは、ファイルを切り捨てずに最初から書き込み用に開くことだけです。通常のリダイレクト>では>>それができませんが、Bash では読み取り/書き込みリダイレクト<>が機能するようです。

echo '#!/bin/sh  ' 1<> foo.sh

または、以下を使用しますdd(これらは標準の POSIX オプションである必要があります)。

echo '#!/bin/sh  ' | dd of=foo.sh conv=notrunc

厳密に言えば、どちらも行末の改行も書き換えますが、それは問題ではないことに注意してください。

もちろん、上記の方法は、指定されたファイルの先頭を無条件に上書きします。元のファイルに正しいハッシュバンがあるかどうかのチェックを追加することは、練習問題として残しておきます...いずれにしても、私はおそらくこれを本番環境では行わないでしょうし、明らかに、行を次のように変更する必要がある場合は機能しません。より長いです1つ。

答え2

{} +最適化するには、 の代わりにを使用します{} \;

find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +

見つかったファイルごとに 1 つの sed プロセスを呼び出す代わりに、ファイルを単一の sed プロセスに引数として提供します。

POSIX仕様の検索{} +(太字は筆者)

プライマリ式が <プラス記号> で区切られている場合、プライマリは常に true として評価され、プライマリが評価されるパス名はセットに集約されます。ユーティリティ utility_name は、集約されたパス名のセットごとに 1 回呼び出されます。

答え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 のアプローチ、ファイルはまったく同じサイズの文字列で上書きされます。違いは次のとおりです。

  • 隠しファイルと隠しディレクトリ内のファイル (たとえば 1 つ) は無視します.git。これらを考慮する可能性は低いためです ( find ./*which を使用すると、現在のディレクトリの隠しファイルとディレクトリはスキップされますが、サブディレクトリの隠しファイルとディレクトリはスキップされません)。これらが必要な場合は、glob 修飾子を追加しますD
  • 置き換える元のシェバンを保持するのに十分な大きさでないファイルについては、わざわざ調べる必要はありません (.を と同等のものとして使用する-type fため、すでにファイルから inode 情報を取得しているので、そこでサイズをチェックしたほうがよいでしょう)。
  • 実際には、ファイルが置換する正しいシェバンで始まっているかどうかをチェックし、必要なバイト数を少なく読み取ります (zsh他のシェルは任意のバイト値を処理できないため、ここではそうする必要があります)。
  • #!/bin/sh -スクリプトの正しいシェバンである を代わりに使用しています/bin/sh(ちなみに はスクリプト#!/bin/bash -の正しいシェバンです)。/bin/bash「#! /bin/sh -」シェバンに「-」があるのはなぜですか?詳細については。

ファイルの上書きに関するエラーは終了ステータスで報告されますが、ディレクトリ ツリーのトラバースに関するエラーやファイルの読み取りに関するエラーは報告されません (ただし、これらを追加することはできます)。

いずれにせよ、それはその通り #!/bin/bash、、、などbashのようにインタープリタとして使用する他のシバンは使用しません。それらについては、何をすべきかを決定する必要があります。はオプションですが、たとえば同等のものはありません。#! /bin/bash#! /bin/bash -Oextglob#! /usr/bin/env bash#! /bin/bash -efu-efush-Oextglobsh

次のような最も簡単なケースをサポートするように拡張できます。

#! /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

ここでは、サポートされているオプションが多数あるさまざまなシェバンが許可され、新しい/bin/shシェバンで再現され、元のシェバンと同じサイズになるように右側がパディングされます (r[length]パラメータ拡張フラグを使用)。

答え4

ファイルは 1 つの長い連続したバイト範囲です。bashをに置き換えるにはsh、基本的に を構成する 2 バイト (UTF-8 または類似のものを想定) を削除する必要がありますba。 ファイルには穴があいていないため、 から始まるすべてのものは、shファイルの 2 バイト前に書き込まれる必要があります。

これには、ファイル全体の書き換え、または少なくとも変更された部分からの書き換えが必要です。

方法はある交換するファイル内のバイト数(たとえば、フォーマットで許可されている場合は無害なスペースを含む)を、ファイル全体を書き換えずに、受け入れられた回答を参照してください。

関連情報