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

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

最も効率的な方法で、何百ものファイルの最初の行を再帰的に変更したいと思います。私がしたかった作業の例#!/bin/bashはに変更することでした#!/bin/shので、次のコマンドを思い出しました。

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

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

答え1

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

...しかし、この場合、ワイヤの長さを実際に変更する必要はありません。#!/bin/sh␣␣ハッシュバン行を2つの末尾のスペースに置き換えることができます。オペレーティングシステムは、hashbang行を解析するときにこれらの内容を削除します。 (または2つの改行または改行+ポンド記号を使用してください。どちらもシェルが最終的に無視する追加の行を生成します。)

私たちがしなければならないのは、ファイルを開いてファイルを切り取るのではなく、最初から書き込むことだけです。通常のリダイレクトではこれを行いませ>>>が、Bashでは読み書きリダイレクトが<>機能しているようです。

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

またはdd、以下を使用してください(標準POSIXオプションでなければなりません)。

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

厳密に言えば、これらの両方は行末の改行文字を書き換えますが、問題ではありません。

もちろん、上記は与えられたファイルの先頭を無条件に上書きします。元のファイルに正しいハッシュバンがあることを確認するものを追加することは練習のままです...とにかく、本番ではこれを行いません。明らかに行を次に変更する必要がある場合もっと長く一つ。

答え2

{} +1つの最適化は代わりに使用することです{} \;

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

見つかったファイルごとにsedプロセスを呼び出すのではなく、単一のsedプロセスの引数としてそのファイルを指定します。

検索のPOSIX仕様{} +(太字で):

基本式が<プラス記号>で区切られている場合、基本式は常にtrueと評価され、基本式が評価されるパス名はコレクションとして集計されます。ユーティリティutil_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たとえば、1つを考慮)のファイルを無視します。なぜなら、これらのファイルを考慮したくないからです(使用中のファイルはfind ./*現在のディレクトリの隠しファイルとディレクトリをスキップしますが、サブディレクトリの隠しファイルとディレクトリはスキップしません)。D必要な場合は、glob修飾子を追加してください。
  • 置き換える元のshebangを入れるのに十分な大きさでないファイルを見つけることには気にしません。 (.equivalentを使用している-type fので、ファイルからすでにinode情報を取得しているので、そこでサイズを確認することをお勧めします。)
  • 私たちは実際にファイルが置き換える正しいshebangで始まっていることを確認し、必要なだけ少ないバイトを読み取ります(zsh他のシェルは任意のバイト値を処理できないため、ここで必要です)。
  • 私たちは#!/bin/sh -これがスクリプトの正しいshebang /bin/sh#!/bin/bash -正しいshebangになるでしょう/bin/bash)を代替品として使用します。バラよりなぜ"#!/bin/sh -" shebangに "-"があるのですか?もっと学ぶ。

ファイル上書きエラーはシャットダウン状態に報告されますが、ディレクトリツリーナビゲーションエラーは報告されず、ファイル読み取りエラーは追加される可能性がありますが報告されません。

とにかくちょうど交換正確に #!/bin/bash、 、bashのような通訳として使用される他のshebangの代わりに 。これを行うには、何をすべきかを決定する必要があります。オプションですが、対応する項目はありません。#! /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右側のパディング(パラメータ拡張フラグを使用)され、新しいshebangで再現されます。r[length]

答え4

ファイルは長いバイトシーケンスです。置き換えるには、bashデフォルトでsh2バイトを削除する必要があります(UTF-8またはそれに似ていると仮定)ba。ファイルには穴がないため、最初からすべての内容をsh2バイト前のファイルに書き込む必要があります。

これを行うには、ファイル全体を書き換えるか、少なくとも変更された部分から始める必要があります。

これを行うにはいくつかの方法があります。変えるたとえば、形式が許可されている場合は、ファイル全体を書き換えずに無実のスペースがある場合は、許可された回答を参照してください。

関連情報