Sed で文字列置換をするときはリダイレクトではなく -i オプションを使う


久々に sed を使ったらハマったので備忘録としてメモ。

やりたかったこと

以下のようなファイルがありました。

$ cat sample.txt
password=secret_password

この secret_passworddummy_password に書き換えたいと思いました。

sed だと次のようにすれば書けます。

$ sed -e 's/secret_password/dummy_password/' sample.txt
password=dummy_password   

出力を見る限り問題なさそうなので、これを元ファイルにリダイレクトしましょう。


$ sed -e 's/secret_password/dummy_password/' sample.txt > sample.txt
$ cat sample.txt
$ wc sample.txt
0 0 0 sample.txt      

…あれ?

なぜファイルが空になるのか

結論から言うと、リダイレクトの仕様によるものです。
UNIX 系のシェルでは > でリダイレクトを行うと、まずリダイレクト先のファイルを空で初期化します。
つまり、 sed が実行される時点で sample.txtの中身は空になっているのです。
空のファイルに対して置換を実行しても、結果が空文字になるのは当たり前で、それを書き込んでも当然空のファイルになります。

これは sed に限らず、 cat なんかでも同じ事が起こせます。

$ touch sample.txt
$ echo "abc" > sample.txt
$ cat sample.txt
abc
$ cat sample.txt > sample.txt
$ cat sample.txt
$ 

同じファイルに対してリダイレクトする、というのは危険です。

ちなみに、一次ソースを探して Bash のドキュメント も見てみたのですが

> filename

# The > truncates file "filename" to zero length.
# If file not present, creates zero-length file (same effect as 'touch').
# (Same result as ": >", above, but this does not work with some shells.)

と書かれているだけで、コマンドを実行した場合の実行順序までは書かれていませんでした。
が、実行してみたら確かにファイルが空になるので、そうなんだろうと納得するしかないのですが、イマイチ釈然としません…

Sed で置換を行う場合は -i オプションを使う

じゃあ、どうすればいいのかという話です。

Sed は -i (または --in-place) というオプションをつけることで、実行結果にファイルを置換することができます。
なので、最初の例の正解はこうなります。

$ cat sample.txt
password=secret_password
$ sed -i -e 's/secret_password/dummy_password/' sample.txt
$ cat sample.txt
password=dummy_password

無事置換ができました。Sed の使い方が間違っているのかと思ったら、そもそも UNIX シェルの仕様を理解していなかったという話でした。

なお、上記は Linux 等で使われる GNU Sed の話です。 macOS や FreeBSD などに入っている BSD Sed については微妙にコマンドが違います。こちらの記事がとても分かりやすく説明しています。

参考