質問
ファイルの最後の行を除くすべての行を削除しようとしましたが、file.txt が空ではないにもかかわらず、次のコマンドは機能しませんでした。
$cat file.txt |tail -1 > file.txt
$cat file.txt
なぜそうなるのでしょうか?
解決
パイプラインを介してファイルから同じファイルにリダイレクトすることは安全ではありません。もし file.txt
前にパイプラインの最終ステージを設定するときにシェルによって上書きされます。 tail
最初のステージから読み取りを開始すると、空の出力が得られます。
代わりに次のことを実行してください。
tail -1 file.txt >file.txt.new && mv file.txt.new file.txt
...まあ、実際には、実稼働コードではそれを行わないでください。特にセキュリティが重要な環境にいて、root として実行している場合は、次の方が適切です。
tempfile="$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt
別のアプローチ (例外的に一時ファイルを避ける) <<<
プラットフォーム上に暗黙的に作成されます) は次のとおりです。
lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"
(上記の実装は bash 固有ですが、echo が機能しない場合でも機能します。たとえば、最後の行に「--version」が含まれている場合などです)。
最後に、次のスポンジを使用できます。 もっと見る:
tail -1 file.txt | sponge file.txt
他のヒント
sed を使用すると、ファイルから最後の行を除くすべての行を削除できます。
sed -i '$!d' file
- -私 sed にファイルを所定の場所に置き換えるよう指示します。それ以外の場合、結果は STDOUT に書き込まれます。
- $ ファイルの最後の行に一致するアドレスです。
- d 削除コマンドです。この場合、それは次のように否定されます。 !, 、つまりすべての行 ない アドレスに一致するものは削除されます。
「cat」が実行される前に、Bash は書き込み用に「file.txt」をすでに開いており、その内容は消去されています。
一般に、同じステートメント内で読み取り元のファイルに書き込まないでください。これは、上記のように別のファイルに書き込むことで回避できます。
$cat file.txt | tail -1 >anotherfile.txt $mv anotherfile.txt file.txtまたはスポンジなどのユーティリティを使用して、 もっと見る:
$cat file.txt | tail -1 | sponge file.txtこれが機能するのは、スポンジが出力ファイルを開く前に入力ストリームが終了するまで待機するためです。
コマンド文字列を bash に送信すると、次のことが行われます。
- I/Oパイプを作成します。
- 「/usr/bin/tail -1」を開始し、パイプから読み取り、file.txt に書き込みます。
- 「/usr/bin/cat file.txt」を開始し、パイプに書き込みます。
「cat」が読み取りを開始するまでに、「file.txt」はすでに「tail」によって切り詰められています。
これはすべて Unix とシェル環境の設計の一部であり、元の Bourne シェルにまで遡ります。これは機能であり、バグではありません。
tmp=$(tail -1 file.txt);echo $tmp > file.txt;
これは Linux シェルでうまく動作します。
replace_with_filter() {
local filename="$1"; shift
local dd_output byte_count filter_status dd_status
dd_output=$("$@" <"$filename" | dd conv=notrunc of="$filename" 2>&1; echo "${PIPESTATUS[@]}")
{ read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output"
(( filter_status > 0 )) && return "$filter_status"
(( dd_status > 0 )) && return "$dd_status"
dd bs=1 seek="$byte_count" if=/dev/null of="$filename"
}
replace_with_filter file.txt tail -1
dd
の「notrunc」オプションは、フィルタリングされたコンテンツを元の場所に書き戻すために使用されます。 dd
実際にファイルを切り詰めるには、(バイト数とともに) が再度必要になります。新しいファイル サイズが古いファイル サイズ以上の場合、2 番目のファイル サイズは dd
呼び出しは必要ありません。
ファイル コピー方法と比較したこの方法の利点は次のとおりです。1) 追加のディスク容量が不要、2) 大きなファイルのパフォーマンスが向上、3) 純粋なシェル (dd 以外)。
Lewis Baumstark が言うように、同じファイル名に書き込むのは好ましくありません。
これは、「cat file.txt」が実行される前に、シェルが「file.txt」を開いてリダイレクトを行うためにそれを切り詰めるためです。だから、あなたはそうする必要があります
tail -1 file.txt > file2.txt; mv file2.txt file.txt
echo "$(tail -1 file.txt)" > file.txt
この場合にのみ使用できます
cat < file.txt | (rm file.txt; tail -1 > file.txt)これにより、「(...)」のサブシェルを持つ接続「cat」の直前に「file.txt」が開きます。「rm file.txt」は、サブシェルが「tail」の書き込みのためにディスクを開く前にディスクから参照を削除しますが、内容は標準入力が閉じるまで「cat」に渡される開かれた記述子を通じて引き続き利用できます。したがって、このコマンドが完了しないと「file.txt」の内容が失われることを確認したほうがよいでしょう。
同じファイル名に書き戻すという事実が気に入らないようです。次のようにすると機能します。
$cat file.txt | tail -1 > anotherfile.txt
tail -1 > file.txt
ファイルが上書きされ、再書き込みが行われるため、cat は空のファイルを読み取ることになります。 前に パイプライン内のコマンドはいずれも実行されます。