Bash で読み込んだファイルに対して、リダイレクトで上書きするとファイルが空になる問題への対処

Bash

はじめに

Bash に限らずシェルスクリプトでも発生する事象ですが、 cat< で読み込んだファイルに対して > (リダイレクト) で上書きを行ったにも関わらず、ファイルが空になってしまうという事象に一度は遭遇したことがある方も多いでしょう。

シェル操作を行うことが多い貴兄にはよくある話ですが、この問題への対処法を紹介します。

検証環境

$ uname -moi
arm64 unknown Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.1.16(1)-release (aarch64-apple-darwin21.1.0)

「リダイレクトで上書きするとファイルが空になる」問題とは?

対処法の話をする前に、「リダイレクトで上書きするとファイルが空になる」問題とは何かについて説明しておきます。

例えば、以下のようなファイルがあるとします。

$ seq 1 10 > data.txt

$ cat data.txt
1
2
3
4
5
6
7
8
9
10

1~10 の数値が書かれている data.txt というファイルを用意しました。
このファイルにから、 25 の数値が登場する行を除外し、 data.txt に上書きします。

以下のようなコマンドを実行すれば、 25 の数値を含む行が除外されることを確認できます。

$ cat data.txt | grep -v -e 2 -e 5
1
3
4
6
7
8
9
10

実行結果を data.txt に書き込みます。

$ cat data.txt | grep -v -e 2 -e 5 > data.txt

data.txt の中身を確認してみます。

# ファイルの中身は空になっている
$ cat data.txt

$ ls -la data.txt
-rw-r--r-- 1 root root 0  7月 28 07:11 data.txt

このように、リダイレクトされたファイルは空になってしまいます。

原因

なぜファイルが空になってしまうのか。

先程のコマンドを少し変更してみます。
パイプの真ん中に tee hoge.txt というコマンドを挿入します。
hoge.txt ファイルには grep に渡る直前の情報が保存されます。

$ cat data.txt | tee hoge.txt | grep -v -e 2 -e 5 > data.txt

# ファイルの中身は空になっている
$ cat hoge.txt

つまり、 cat data.txt の実行結果が空になっていることがわかります。

パイプで繋いだコマンドの一番最後に記述されている > data.txt がポイントです。

コマンド全体が実行されると、一番最初に > data.txt が実行され、ファイルが空にされてしまうのです。

処理ステップを文字で表現すると以下のようになリます。

  1. cat data.txt : data.txt を読み込む準備を行う(Open)
  2. > data.txt : data.txt に書き込む準備を行う(Open)
  3. data.txt を読み込む
  4. grep -v -e 2 -e 5 : 25 を含む行を除外 ( 入力が空なので、出力結果も空 )
  5. data.txt に書き込む

対処法 1 : 一時的に別のファイルに書き出す

そもそも「上書き」をやめる、というのが一番確かです。

tmp.txt という一時ファイルに結果を書き出した後、 data.txt にリネームしてみます。

$ cat data.txt | grep -v -e 2 -e 5 > tmp.txt

$ mv tmp.txt data.txt

$ cat data.txt
1
3
4
6
7
8
9
10

問題なく結果が保存されました。

対処法 2 : 「プロセス置換(+ファイル削除)」を使う

プロセス置換を使ったハックを行うことで、この問題に対処することができます。

リダイレクトで結果をファイルにリダイレクトする前に、削除

# リダイレクト直前にファイルを削除
$ cat data.txt | (rm data.txt; grep -v -e 2 -e 5 > data.txt)

# 中身を確認
$ cat data.txt
1
3
4
6
7
8
9
10

もしプロセス置換を使ってもファイルの削除処理を行わなければ、やはり空になります。

$ cat data.txt | (grep -v -e 2 -e 5 > data.txt)

# ファイルの中身は空になっている
$ cat data.txt

処理ステップを文字で表現すると以下のようになリます。

  1. cat data.txt : data.txt を読み込む準備を行う(Open)
  2. rm data.txt : data.txt を削除
  3. > data.txt : data.txt に書き込む準備を行う(Open)
  4. data.txt を読み込む
  5. grep -v -e 2 -e 5 : 25 を含む行を除外 ( 入力が空なので、出力結果も空 )
  6. data.txt に書き込む

お恥ずかしながら、なぜこの方法で上書きファイルが空になる問題が解消されるか理解できておりません。
若い頃に先輩エンジニアに教えていただいた方法です。

ひとこと

rm を実行するのは少し怖いこともあり、可能なら一時ファイルを作成した後 mv で上書きするほうが確実かつ可読性が髙いです。

Bash