少し前(というか2019年の年末)に、Twitterでファイルをコマンドで利用したあとにリダイレクトで上書きしてはいけないよねというツイートがあった。

これは結構やってしまう(そしてやらかして学習する)人が多いと思うのだけど、shellの解釈の順序の関係でコマンドの処理より先にリダイレクトの処理が評価されてしまう。

そのため、これを実行するとコマンドの実行前に上書きする対象のファイルの中身が空っぽになってしまい、空のファイルが出来上がることになる。

で、元あったファイルを編集して上書きする場合だとどんな方法があるのかなと思ったので、少しまとめてみることにした(個人的にはmoreutilsのspongeを使うパターンが多い)。

(GNU sedだったら普通に-i使えば良いんだろうけど、それ以外のコマンドで編集することも多々あると思うので、そういった方法については記述していない)

1. moreutilsのspongeコマンドを利用する

これが多分一番ラクでわかりやすいのではないだろうか。

moreutilsというパッケージに入っているspongeコマンドを利用することで、同じファイルを利用して上書き処理ができる。

command... file | sponge file
[blacknon@BlacknonMacBook-Pro2018][~/Work/202001/20200103]
(`・ω・´)  < cat a.txt
a01 a02 a03 a04 a05
b01 b02 b03 b04 b05
c01 c02 c03 c04 c05
d01 d02 d03 d04 d05

[blacknon@BlacknonMacBook-Pro2018][~/Work/202001/20200103]
(`・ω・´)  < sed -r 's/ |^/&x_/g' a.txt | sponge a.txt

[blacknon@BlacknonMacBook-Pro2018][~/Work/202001/20200103]
(`・ω・´)  < cat a.txt
x_a01 x_a02 x_a03 x_a04 x_a05
x_b01 x_b02 x_b03 x_b04 x_b05
x_c01 x_c02 x_c03 x_c04 x_c05
x_d01 x_d02 x_d03 x_d04 x_d05

2. グループにヒアドキュメントで渡す

これは人から教えてもらった手法なのだが、POSIXでも動作する方法らしい。

以下のように、グループ化したコマンドに対しヒアドキュメントでファイルを渡すことで元のファイルの内容を編集した結果を上書きできる。

(rm file; command ... < file) > file

詳細についてはリンク先を読んでもらったほうが良いと思うのだが、グループ内で先頭に記述しているrmを行うことで別のinodeを利用させることで処理するようだ。

3. 「1>」 でリダイレクトする

これはぐれさんのツイートを見て知った方法で、bash(あと確認した限りzshでも)動作する方法らしい(実は今まで知らなかったのだが、bashのmanにも載っているやり方のようだ)。

command ... file 1<> file

bashの機能として、「<>」で読み書きモードでファイルを開くことができ、「n <>」で指定したファイルディスクリプタを利用することができるらしい。

bashの仕様上、読み書きモードで開く際はファイルの全消去をしない挙動のため、このようなことができるらしい。これは知らなかった…(もしくはどっかで見かけてたのに覚えてなかった)。

詳細については、ぐれさんがSoftware Designの4月号に記事を書いていたので、気になる場合はそちらを参照。

4. 一度ファイルの中身を変数に格納する

(まぁこれはわざわざ書かなくてもいいかなという気もするのだけど…) 一度ファイルの中身を変数に格納して、それを利用してファイルに書き込み処理をする普通のやり方。

data="$(< file)"
command ... <<<"${data}" > file

思っていたよりもいろいろとやり方があることを知った。