パイプを通じて単一のファイルを入力と出力の両方として処理する

パイプを通じて単一のファイルを入力と出力の両方として処理する

こんばんは、

パイプ コマンドを使用してファイルの内容をフィルターし、その結果を同じファイルに書き戻したいと思います。分かっていますが、私が書いた方法ではそれができません。ちょっと待ってください...

これは私が持っている bash スクリプトの一部です。

grep '^[a-zA-Z.:]' "$filepath" \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$filepath"

そこで、代わりにプロセス置換を使用すれば成功できると考えました。そこで次のように書きました。

grep '^[a-zA-Z.:]' < <(cat "$filepath") | …

これも何も解決しませんでした。プロセス置換によって、入力ファイルのコンテンツが一時ファイルなどのどこかに「保存」されると思っていました。プロセス置換も理解できていないようです。

sed -i私は「インプレース」エディションに関するスレッドを読みましたが、これらの記事ではやなどの一部のバイナリの特別なオプションが強調されていましたsort -oが、一般的なソリューションが必要です (つまり、パイプされたコマンドに適合する必要があります)。

まず、「パイプの標準的な方法」ではなぜこれができないのか、その裏で何が起こっているのか?:/そして、どうすれば問題を解決できるのか?誰か説明するこれは一体何なんでしょうか?

ありがとう。

答え1

すでに述べたように、スポンジはもっと見る素晴らしいです。moreutils への依存を避けるために、このスクリプトを使用してエミュレートします。

#!/bin/sh -e
#Soak up input and tee it to arguments
st=0; tmpf=
tmpf="`mktemp`" && exec 3<>"$tmpf" || st="$?"
rm -f "$tmpf" #remove it even if exec failed; noop if mktemp failed
[ "$st" = 0 ] || exit "$st"
cat >&3
</dev/fd/3 tee "$@" >/dev/null

次のように使用できます:

grep '^[a-zA-Z.:]' "$filepath" \
| sed -r '/^(rm|cd)/d' \
| uniq -u | sponge "$filepath" 

リダイレクトはコマンドが開始される前に行われ、出力リダイレクトによって出力ファイルが切り捨てられるため、単純な出力リダイレクトではこれを実行できません。

つまり、grep (パイプラインの最初の単純なコマンド) が開始されるまでに、最後のリダイレクトによって入出力ファイルがすでに切り捨てられています。

私の知る限り、真のインプレース編集を行う標準 UNIX ユーティリティは実際には存在しません。sed -i一時ファイルでのみエミュレートします。その理由は、真のインプレース フィルタリングでは、パイプライン ステップが失敗するとファイルが簡単に破損する可能性があるためだと思います。

裏で何が起こっているかと言うと、 と は両方とも|<()パス IO を一度に 1 つのバッファで受け取るシステム パイプを使用します。このメカニズムは一時ファイル (実際の (ファイルシステム) ファイルではない) を作成せず、入力全体を一度にメモリ内に保持しないようにします。

答え2

同じファイルからの入力と出力をしたい場合は、以下を試してください。スポンジ説明には次のように書かれています。

sponge reads standard input and writes it out to the specified file. 
Unlike a shell redirect, sponge soaks up all its input before writing 
the output file. This allows constructing pipelines that read from and 
write to the same file.

sed '...' file | grep '...' | sponge [-a] fileつまり、入力を受け取るようなものを作ることができますファイル同じ出力ファイル


一方、一時ファイルを使用することは、入力と出力に同じファイルを使用する優れた方法でもあります。一時ファイルは次のように初期化できます。

tempfile=`mktemp tempFile.XXXX` # You can replace "tempFile" with any name you want

これにより、このスクリプトが実行されるディレクトリに、「tempFile」という一時ファイルが作成されます。このファイルの拡張子は「XXXX」で、x は現在のプロセス番号とランダムな文字の組み合わせに置き換えられます (例: tempFile.AVm7)。

これで、パイプ (またはパイプされたコマンド) を次のように変更できます。

grep '^[a-zA-Z.:]' "$filepath" \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$tempfile"

フィルター処理後、次のようにして一時ファイルを元のファイルに移動できます。

mv "$tempfile" "$filepath"

これにより、一時ファイルが削除され、フィルターされた元のファイルが残ります。ただし、場合によっては、必要のない一時ファイルが大量に作成され、破棄されないことがあります。そのため、スクリプトの終了後に、不要になったすべての一時ファイルを削除してディレクトリをクリーンアップすることをお勧めします。そのためのルーチンは、次のように記述できます。

remove_temp_files() {
    rm `find . -name "tempFile.????"`
}

その後、スクリプトの最後にルーチンを呼び出すだけでremove_temp_files、上記の形式で作成されたすべての一時ファイルを削除できます。

答え3

使用ヒアドキュメントそしてコマンド置換この場合の標準的な方法は次のとおりです。

grep '^[a-zA-Z.:]' <<IN \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$filepath"
$(cat -- "$filepath")
IN

その他の質問については、以前の多くの質問で説明されています。

関連情報