bashでファイルを利用して処理を行い、その結果を処理に使ったファイルに上書き・追記するといった場合、普通にやると対象のファイルが空になってしまう。

[root@BS-PUB-CENT7-01 ~]# cat -n test.txt
     1  a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
     2  b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
     3  c01 c02 c03 c04 c05 c06 c07 c08 c09 c10
     4  d01 d02 d03 d04 d05 d06 d07 d08 d09 d10
     5  e01 e02 e03 e04 e05 e06 e07 e08 e09 e10
     6  f01 f02 f03 f04 f05 f06 f07 f08 f09 f10
     7  g01 g02 g03 g04 g05 g06 g07 g08 g09 g10
     8  h01 h02 h03 h04 h05 h06 h07 h08 h09 h10
     9  i01 i02 i03 i04 i05 i06 i07 i08 i09 i10
    10  j01 j02 j03 j04 j05 j06 j07 j08 j09 j10
[root@BS-PUB-CENT7-01 ~]# cat -n test.txt | head -1 > ./test.txt
[root@BS-PUB-CENT7-01 ~]# cat ./test.txt
[root@BS-PUB-CENT7-01 ~]#

これは、上書きだった場合にはリダイレクト実行時に空ファイルが自動的に作られ、そこに実行結果を記述するといった動きをするかららしい。 そのためか、追記は普通にできたりする(grepなど、一部のコマンドはチェック処理が走るためダメ)。

[root@BS-PUB-CENT7-01 ~]# cat -n test.txt
     1  a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
     2  b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
     3  c01 c02 c03 c04 c05 c06 c07 c08 c09 c10
     4  d01 d02 d03 d04 d05 d06 d07 d08 d09 d10
     5  e01 e02 e03 e04 e05 e06 e07 e08 e09 e10
     6  f01 f02 f03 f04 f05 f06 f07 f08 f09 f10
     7  g01 g02 g03 g04 g05 g06 g07 g08 g09 g10
     8  h01 h02 h03 h04 h05 h06 h07 h08 h09 h10
     9  i01 i02 i03 i04 i05 i06 i07 i08 i09 i10
    10  j01 j02 j03 j04 j05 j06 j07 j08 j09 j10
[root@BS-PUB-CENT7-01 ~]# cat -n test.txt | head -1 >> ./test.txt
[root@BS-PUB-CENT7-01 ~]# cat test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
c01 c02 c03 c04 c05 c06 c07 c08 c09 c10
d01 d02 d03 d04 d05 d06 d07 d08 d09 d10
e01 e02 e03 e04 e05 e06 e07 e08 e09 e10
f01 f02 f03 f04 f05 f06 f07 f08 f09 f10
g01 g02 g03 g04 g05 g06 g07 g08 g09 g10
h01 h02 h03 h04 h05 h06 h07 h08 h09 h10
i01 i02 i03 i04 i05 i06 i07 i08 i09 i10
j01 j02 j03 j04 j05 j06 j07 j08 j09 j10
1  a01 a02 a03 a04 a05 a06 a07 a08 a09 a10

では、処理結果をどのように上書きすればよいか。一時ファイルなんて作りたくはない…。しかし、テストしたがファイルディスクリプタを使ってもうまくはいかないので、どうすればいいか。 色々と調べてみたところ、以下の2通りのやり方がありそうだ。

1. プロセス置換を利用する

プロセス置換を利用して、以下のようにコマンドを実行することで上書き処理が行えた。 なお、この際sleepコマンドが無いと、リダイレクトが行われる処理のタイミングによってはそのままからファイルになってしまうことがあるので注意。

コマンド File.path > >( sleep 1 && cat > File.path)
[root@BS-PUB-CENT7-01 ~]# cat test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
c01 c02 c03 c04 c05 c06 c07 c08 c09 c10
d01 d02 d03 d04 d05 d06 d07 d08 d09 d10
e01 e02 e03 e04 e05 e06 e07 e08 e09 e10
f01 f02 f03 f04 f05 f06 f07 f08 f09 f10
g01 g02 g03 g04 g05 g06 g07 g08 g09 g10
h01 h02 h03 h04 h05 h06 h07 h08 h09 h10
i01 i02 i03 i04 i05 i06 i07 i08 i09 i10
j01 j02 j03 j04 j05 j06 j07 j08 j09 j10
[root@BS-PUB-CENT7-01 ~]# head -1 test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
[root@BS-PUB-CENT7-01 ~]# head -1 test.txt > >(sleep 1 && cat > test.txt)
[root@BS-PUB-CENT7-01 ~]# cat test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
[root@BS-PUB-CENT7-01 ~]#

2018/01/26 追記

もしくは、sleepを使わずに変数にファイルの内容を差し込んで、それを上書きさせるようにする。

コマンド File.path > >(VAR=$(cat) && echo "$VAR" > File.path)

2. moreutilsのspongeコマンドを用いる

上記のプロセス置換を利用する方法のほか、moreutilsにあるspongeコマンドを用いることで、そのままファイルを上書きできる。

コマンド File.path | sponge File.path
[root@BS-PUB-CENT7-01 ~]# cat test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
c01 c02 c03 c04 c05 c06 c07 c08 c09 c10
d01 d02 d03 d04 d05 d06 d07 d08 d09 d10
e01 e02 e03 e04 e05 e06 e07 e08 e09 e10
f01 f02 f03 f04 f05 f06 f07 f08 f09 f10
g01 g02 g03 g04 g05 g06 g07 g08 g09 g10
h01 h02 h03 h04 h05 h06 h07 h08 h09 h10
i01 i02 i03 i04 i05 i06 i07 i08 i09 i10
j01 j02 j03 j04 j05 j06 j07 j08 j09 j10
[root@BS-PUB-CENT7-01 ~]# head -2 test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
b01 b02 b03 b04 b05 b06 b07 b08 b09 b10
[root@BS-PUB-CENT7-01 ~]# head -2 test.txt | sponge test.txt
[root@BS-PUB-CENT7-01 ~]# cat test.txt
a01 a02 a03 a04 a05 a06 a07 a08 a09 a10
b01 b02 b03 b04 b05 b06 b07 b08 b09 b10

3.コマンド置換を利用する(2018/04/29 追記)

コマンド置換で実行した結果をechoで呼び出し、それを同じファイルに書き出す方法でも上書きができる。 (コマンド置換の方がリダイレクトよりも先に処理されるので、中身が消えないから)

echo "$(cmd a.txt)" > a.txt
blacknon@BS-PUB-UBUNTU-01:~$ echo {a..c}{01..05} | xargs -n5 > r_test.txt
blacknon@BS-PUB-UBUNTU-01:~$ cat r_test.txt
a01 a02 a03 a04 a05
b01 b02 b03 b04 b05
c01 c02 c03 c04 c05
blacknon@BS-PUB-UBUNTU-01:~$ echo "$(cut -d' ' -f1,3,5 r_test.txt )" > r_test.txt
blacknon@BS-PUB-UBUNTU-01:~$ cat r_test.txt
a01 a03 a05
b01 b03 b05
c01 c03 c05

sedなどのような上書きオプションのあるコマンドであればいいが、それらが無いコマンドで結果を上書きしたい場合、このような方法で(半ば無理やり感はあるけど)上書きは可能なようだ。

4. 1<>で上書きをする(2020年追記)

全然気づかないで似たような内容を記述していたのだが、2020年に書いた内容でもっとシンプルな方法があった。 bashのmanにも書かれているのだが、以下のようにコマンドを実行することで上書きが可能のようだ。

command ... file 1<> file

bashの機能として、「<>」で読み書きモードでファイルを開くことができ、「n <>」で指定したファイルディスクリプタを利用することができるらしい。 これはbashの仕様上、読み書きモードで開く際はファイルの全消去をしない挙動になっているからのようだ。