rm -rf の事故はなぜ起きる|空の変数・空白・cd失敗と防ぎ方

空の変数、1 文字の空白、失敗した cd。rm -rf が意図しない場所を消す仕組みと、実行前の確認方法を説明します。

変数が空だと削除範囲が変わる。削除コマンドの前に、展開後の対象を確認する
変数が空だと削除範囲が変わる。削除コマンドの前に、展開後の対象を確認する

rm -rf は、-r(ディレクトリを中身ごと再帰的に削除)と -f(存在しないファイルを無視し、確認を出さない)を組み合わせたものです。指定した対象を問い合わせなしで消し、Linux のコマンドラインにはごみ箱がありません。そのため事故の多くは「rm -rf そのもの」ではなく、「rm -rf に渡された引数が、本人の思っていたものと違った」ことで起きます。引数を変えてしまう原因はシェルの仕組みにあり、ベテランが rm の行で手を止めるのはその仕組みを知っているからです。

空の変数で削除範囲がルートまで広がる

シェルスクリプトでは、削除するディレクトリを変数で受け取ることがよくあります。変数が設定されていなかったり、前の処理の失敗で空になったりすると、変数の部分は空文字に置き換わります。シェルは空になった結果をそのまま rm に渡すので、本来 /var/app/cache の中身を消すはずの行が、/ の中身を消す行に変わります。

# 危ない例CACHE_DIR=$(cat /etc/app/cache_path)   # ファイルが無いと空になるrm -rf "$CACHE_DIR"/*# 安全な例:空なら止まるrm -rf "${CACHE_DIR:?CACHE_DIR is empty}"/*
CACHE_DIR が空だと rm -rf /* になる

GNU coreutils の rm は既定で --preserve-root が有効で、rm -rf / のように / そのものを指定すると拒否します。しかし /* はシェルが /bin、/etc、/home などに展開してから rm に渡すため、この保護は働きません。${VAR:?メッセージ} はシェルの機能で、変数が未設定または空のときにメッセージを出してコマンドを実行せず、スクリプトなら終了します。set -u も未設定の変数をエラーにしますが、空文字が代入された変数は止めない点が違います。

変数は二重引用符で囲む

DIR="/data/my project" のように空白を含む値を引用符なしで rm -rf $DIR と使うと、シェルが空白で分割し、/data/my と project の 2 つを削除しようとします。変数は "$DIR" と二重引用符で囲み、1 つの引数として渡します。

1 文字の空白が別の削除対象を作る

シェルは空白で引数を区切ります。パスの途中に空白が 1 つ入るだけで、引数が 2 つに分かれ、片方が想定外の場所を指します。手で打ったコマンドや、チャットからコピーしたコマンドで起きやすい誤りです。

rm -rf /var/app/tmp     # 意図:/var/app/tmp を削除rm -rf / var/app/tmp    # 実際:/ と、カレントからの var/app/tmp を削除rm -rf ./*.log          # 意図:.log ファイルを削除rm -rf ./* .log         # 実際:カレントの全ファイルと .log を削除
どちらも空白の位置だけが違う

ダブルチェックでは、rm の行を「引数がいくつあるか」で読みます。スラッシュの直後やアスタリスクの前後に空白がないかを、文字単位で見るのがベテランの読み方です。コマンドを読み上げるときも「アールエム、ハイフンアールエフ、スラッシュ、ヴァー…」と区切りまで口に出すと、空白に気づきやすくなります。

cd の失敗でカレントディレクトリが消える

cd で移動してから相対パスで消す書き方は、cd が失敗したときに危険です。ディレクトリ名の誤字や権限不足で cd が失敗しても、セミコロンや改行で区切られた次の行はそのまま実行されます。その結果、移動前にいたディレクトリ(スクリプトによってはホームディレクトリやアプリケーションのルート)の中身が消えます。

# 危ない例cd /var/app/buidrm -rf *# 安全な例 1:cd が成功したときだけ rm を実行するcd /var/app/build && rm -rf ./*# 安全な例 2:移動せず絶対パスで指定するrm -rf "/var/app/build/"*
cd が失敗したら rm を実行しない

&& は左側のコマンドが成功したときだけ右側を実行します。スクリプト全体の先頭に set -e を書くと、失敗したコマンドの時点で終了するようになりますが、条件式の中など止まらない場面もあるため、rm の直前の cd は && でつなぐほうが確実です。そもそも cd せず絶対パスで書けば、カレントディレクトリに依存しなくなります。

ワイルドカードは rm の前に展開される

* や ? のようなワイルドカードは、rm ではなくシェルが展開します。rm が受け取るのは展開後のファイル名の一覧です。つまり、何が消えるかは実行するまで画面に出てきません。同じ引数で ls や echo を先に実行すると、展開後の一覧を目で確認できます。

ls -d /var/app/releases/2025*echo rm -rf /var/app/releases/2025*rm -rf /var/app/releases/2025*
同じ引数で中身を見てから消す

find を使う場合も注意が必要です。GNU find の -delete は、書かれた順に評価されるため、find . -delete -name '*.tmp' のように条件より前に置くと、名前に関係なくすべてを削除します。まず -delete を外して -print で一覧を出し、結果を確認してから末尾に -delete を付けるのが安全な順番です。

一致するファイルが 1 つも無いとき、bash は既定でワイルドカードを展開せず、*.log という文字列のまま rm に渡します。-f が付いていると「存在しない」というエラーも出ないため、消すべきものが残っていることに気づきません。ls で確認する手順は、消しすぎだけでなく、消し漏れに気づくためにも役立ちます。

消す前に mv で退避する

本番作業では、いきなり削除せず、退避用のディレクトリに mv で移しておく方法がよく使われます。同じファイルシステムの中の mv は名前の付け替えなので、大きなディレクトリでもすぐ終わります。問題が無いことを確認した後で、退避先を削除します。誤って必要なものを移しても、mv で戻せます。

mkdir -p /var/app/trash-20261016mv /var/app/releases/2025* /var/app/trash-20261016/# アプリケーションの動作を確認した後でrm -rf /var/app/trash-20261016
削除を 2 段階に分ける

対話的に使うときは、rm -I も選択肢です。GNU の rm で -I を付けると、3 つより多いファイルを消すときや再帰的に消すときに 1 回だけ確認を求めます。ただし -f と同時に指定すると後に書いたほうが優先されるなど、オプションの組み合わせで挙動が変わるので、確認の代わりにはなりません。

スクリプトで rm を書くときの守り方

手で打つコマンドは実行前に人の目で確認できますが、スクリプトは書いたとおりに毎回実行されます。定期実行やデプロイのスクリプトに含まれる rm は、作成時には正しくても、設定ファイルの変更や環境の違いで変数の中身が変わったときに事故になります。そのため、スクリプトの rm には複数の守りを重ねるのが一般的です。

#!/usr/bin/env bashset -euo pipefailTARGET="${1:?usage: cleanup.sh <dir>}"case "$TARGET" in  /var/app/cache/*) ;;  *) echo "refuse: $TARGET" >&2; exit 1 ;;esacrm -rf --one-file-system -- "$TARGET"
変数のチェックと削除範囲の制限を組み合わせる

set -u は未設定の変数を、set -o pipefail はパイプの途中の失敗をエラーとして扱います。case 文では、削除してよいパスの範囲を先頭一致で限定し、それ以外なら終了します。GNU の rm の --one-file-system は、別のファイルシステムがマウントされたディレクトリに入り込んで削除することを防ぎます。-- はオプションの終わりを示し、- で始まるファイル名がオプションとして解釈されるのを防ぎます。

ベテランが rm の行で手を止めるのは、こうした守りがあるかを確かめるためです。守りが無いスクリプトを見つけたら、その場で実行を止め、少なくとも ${VAR:?} と削除範囲の確認を足してから使うよう指摘します。

rm -rf を実行する前のチェックリスト

見る箇所起きる事故対策
変数空や未設定で /* になる"${VAR:?}" と書く
空白引数が分かれて / やカレントが対象になる引数の数を数えて読む
cd失敗してカレントが消える&& でつなぐか絶対パスにする
ワイルドカード想定より多くのファイルに展開される同じ引数で ls・echo を先に実行する
find -delete条件より前にあると全件が消える-print で確認してから末尾に付ける
  • 削除対象のパスが手順書と 1 文字単位で一致しているか。
  • 接続しているホストが手順書の対象ホストか(ホスト名をプロンプトや hostname で確認する)。
  • 削除してよいか判断できない場合に、mv で退避する手順があるか。
  • 削除後に何を確認すれば完了とするか(ディスク使用量、アプリケーションの起動など)。

rm -rf の事故は、コマンドを知らないことよりも、シェルが引数をどう組み立てるかを意識していないことから起きます。下の問題で、危ない rm の行を見抜けるか確かめてみてください。

問題で確かめる

この記事の内容を、制限時間つきの問題で確かめられます。

参考

よくある質問

rm -rf で消したファイルは復元できる?

Linux の rm にはごみ箱が無く、通常の操作では戻せません。バックアップやスナップショットから戻すのが基本で、復元ツールでも確実には取り戻せません。

rm -rf / を実行するとどうなる?

GNU coreutils の rm は既定の --preserve-root により / そのものの削除を拒否します。ただし rm -rf /* はシェルがルート直下の各ディレクトリに展開するため、この保護は働きません。

${VAR:?} とは何?

シェルのパラメータ展開の 1 つで、変数が未設定または空のときにエラーメッセージを出してコマンドを実行しません。rm -rf "${DIR:?}"/* のように使うと、空の変数による削除範囲の拡大を防げます。

rm -r と rm -rf の違いは?

-f を付けると、書き込み保護されたファイルの確認を出さず、存在しないファイルを指定してもエラーにしません。確認が出なくなる分、誤りに気づく機会が減ります。