rm -rf の全ファイル削除の事故を防ぐ方法

メインPCで「rm -rf」をやってしまい、ホームディレクトリ下を全削除してしまいました。デスクトップのランチャーが消え、最上部バーも消えてしまいました。やってしまった!と気づいて、コントロールCで中止しました。

やっちまったなー

ログアウトしたら、二度とログインできない気がして、ログアウトせず、リストア作業をしました。

幸いなことに、リモートのgitリポジトリとデイリーバックアップから復活できました。デイリーバックアップがあったとはいえ、心臓に悪いです。リストアと確認に数時間かかりました。

経緯はこうです

bashスクリプトを作っていました。次のようなコードでした。

#!/bin/bash

rm -rf   ~/tmp/xxx/*
mkdir -p ~/tmp/xxx/

mkdir -p ~/tmp/xxx/yyy
cd       ~/tmp/xxx/yyy
〜省略〜Code language: Bash (bash)

複数箇所に"~/tmp/xxx"を書いていたので、一箇所にするために、変数にしました。

#!/bin/bash

rm -rf   $TMP_USER_HOME/*
mkdir -p $TMP_USER_HOME/

mkdir -p $TMP_USER_HOME/yyy
cd       $TMP_USER_HOME/yyy
〜省略〜Code language: Bash (bash)

何が悪いの?

TMP_USER_HOMEが未設定ね。
3行目が「rm -rf /*」になったのね!

...

/bin や /etc はroot所有なので、「許可がありません」と表示されて、削除されませんでした。

しかし、自分のホームディレクトリ下は・・・削除されました。

次のように、「TMP_USER_HOME=~/tmp/xxx」をするつもりでしたが、忘れていました。

#!/bin/bash

TMP_USER_HOME=~/tmp/xxx

rm -rf   $TMP_USER_HOME/*
mkdir -p $TMP_USER_HOME/

mkdir -p $TMP_USER_HOME/yyy
cd       $TMP_USER_HOME/yyy
〜省略〜Code language: Bash (bash)

今回の事故は、操作ミスによる削除、作ったシェルスクリプトのバグ、2つの側面があります。2つの側面から、対策を考えました。

対策まとめ

方法効果難易度コメント
trash-cli★★★簡単に使えて、効果大
bash nounset オプション★★★未設定の変数をエラーにする
dockerコンテナで作業する★★★★★★破壊的な処理をしても、その影響を限定的にする対策
echoで確認するプログラマのモラルに依存
m4マクロで確認する★★★★プログラマのモラルに依存
(非推奨)
"-i" ファイルを設置する
カレントディレクトリに "-i"ファイルを作っておくと、rm -f * を実行したとき、確認プロンプトが表示される。

trash-cli

効果★★★
難易度
GitHub - andreafrancia/trash-cli: Command line interface to the freedesktop.org trashcan.
Command line interface to the freedesktop.org trashcan. - andreafrancia/trash-cli

trash-cliはゴミ箱に捨てるコマンド群です。そのうちの trash-put がゴミ箱に捨てるコマンドです。

エイリアス設定をしておき、rmコマンドが呼ばれたら、trash-putを呼ぶという解決方法です。

  • trash-put - ゴミ箱に移動する
  • trash-list - ゴミ箱の一覧
  • trash-restore - 元に戻す
  • trash-empty - ゴミ箱を空にする

macOS

移動先は、デスクトップのゴミ箱ではなく、独自の領域です。trash-putしても、デスクトップのゴミ箱に表示されません。

trash-list、trash-empty、trash-restoreでゴミ箱を操作します。

homebrewでインストールすると、0.17.xがインストールされます。

$ brew install trash-cliCode language: Bash (bash)

Ubuntu

trash-putすると、デスクトップのゴミ箱に表示されました。

aptでインストールすると、0.12.xがインストールされます。0.12.xには、trash-restoreがありません。ファイルマネージャーnautilusでゴミ箱から元に戻します。

$ sudo apt install trash-cliCode language: Bash (bash)

0.17.xを使いたい場合は、githubからをgit cloneして、インストールします。

rmのエイリアスにtrash-putを設定する

~/.bash_aliases に、rmのエイリアスに trash-put を設定します。

# 2019-06-04 aoki
if type trash-put &> /dev/null
then
    alias rm=trash-put
fi
Code language: Bash (bash)

.bashrcがなければ新規作成し、次の内容を追加しておきます。

if [ -f ~/.bash_aliases ]; then
    source ~/.bash_aliases
fiCode language: Bash (bash)

.bashrcを適用します。rmがエイリアス設定されたかを確認してください。

$ . ~/.bashrc

$ alias rm
alias rm='trash-put'Code language: Bash (bash)

これで、次のようにrmを叩いても、ゴミ箱に移動されるようになります。それでも、rmは使わないようにして、trash-putを使うようにしたほうがいいですね。

$ rm myhoge.txt
$ rm -rf hoge

$ trash-list
〜省略〜Code language: PHP (php)

bash nounset オプション

効果★★★
難易度

nounsetオプションを有効にすると、未設定の変数を参照する箇所で、エラー停止します。

このオプションを有効にしていれば、変数を設定していないことに気づき、今回の事故は防ぐことができたでしょう。

nounsetオプションを有効にするには、次のように記述します。

#!/bin/bash -u
Code language: Bash (bash)
#!/bin/bash
set -uCode language: Bash (bash)
#!/bin/bash
set -o nounsetCode language: JavaScript (javascript)

未設定の変数を参照していると、エラーで停止します。

$ ./test.sh
./test.sh: 行 3: TMP_USER_HOME: 未割り当ての変数ですCode language: Bash (bash)

エラー、安全性、デバッグに関するbashオプション

変数未定義のチェック-o nounset-u
エラー発生時終了-o errexit-e
デバッグ出力-o xtrace-x

参考
SoftwareDisign 2019年6月号 第1特集 思わず実践したくなるシェル&シェルスクリプト p59

dockerコンテナで作業する

効果★★★
難易度★★★

プロジェクトディレクトリをdockerコンテナにマウントして、bashログインして作業します。

dockerコンテナ内で、rm -rf で全削除してしまっても、git commit、git pushしていないファイルを失うだけで済みます。

削除してしまっても、gitで最後のcommitまで戻するか、git cloneして、dockerコンテナをdown/upするだけで、やり直しできます。

同じコンセプトで、VirtualBoxやVagrantの仮想マシンで作業する方法もありますが、dockerコンテナを起動するほうが時間がかからないので手軽です。

echoで確認する

効果
難易度

プログラマのモラルに依存した方法です。

さきほどのスクリプトで言うと、まず、echoして確認してから、echoをはずします。

実際、面倒ですし、重要な処理の箇所では、この方法を使ってきましたが、今回の事故は起きてしまいました。

#!/bin/bash

echo "rm -rf   $TMP_USER_HOME/*"
echo "mkdir -p $TMP_USER_HOME/"

echo "mkdir -p $TMP_USER_HOME/yyy"
echo "cd       $TMP_USER_HOME/yyy"
〜省略〜Code language: Bash (bash)

m4マクロで確認する

効果★★
難易度★★

これも、プログラマのモラルに依存した方法です。

さきほどのechoは、確認のたびに、echoのオン/オフを編集する必要がありました。この方法は、常にecho相当の内容を吐き出します。一種のドライランです。

m4マクロを記述しておき、bashスクリプトを吐き出します。m4マクロによって、変数が展開されているので、処理を確認しやすくなります。確認してから、それを実行します。

まず、sample.m4を作ります。

define(TMP_USER_HOME, ~/tmp/xxx)

rm -rf TMP_USER_HOME/*Code language: Bash (bash)

sample.m4 を m4にかけて、sample.shに保存します。目視でsample.shを確認します。

$ m4 sample.m4 >sample.shCode language: Bash (bash)

bashでsample.shを実行します。

$ bash sample.shCode language: Bash (bash)

問題がなければ、1行で実行します。

$ m4 sample.m4 | bashCode language: Bash (bash)

結局、1行で実行できてしまいます。開発途中の事故は防げても、修正時の事故は起こりそうですね。

(非推奨)"-i" ファイルを設置する

効果
難易度

この方法はおすすめできないわ

検索していたら、この内容の記事が2件ありました。rmにこんな機能があったの?と驚きましたが、実際に試して検証してみたら、シェルのアスタリスク展開を利用したトリックでした。

これはトリッキーな方法です。次のような問題があるので、おすすめしません。

仕組み

rmコマンドに -i オプションをつけると、「削除しますか?」と確認プロンプトが表示されます。

カレントディレクトリに、a.txtがあるとして、あらたに -i ファイルを作ります。

$ touch ./-iCode language: Bash (bash)

rm -f * を実行すると、シェルがアスタリスクをカレントディレクトリのファイル一覧(-i a.txt)に展開して、rmコマンドに渡します。

つまり、rm -f -i a.txt を実行します。

いつのまにか、-i オプションを指定したことになり、「削除しますか?」確認プロンプトが表示される、という仕組みです。

効果のない場合がある

ただし、-i ファイルを置いたディレクトリは、常にrmから保護されるかというと、そうではありません。

次の呼び出しには効果があります。-f オプションをつけているのに、削除しますか?と確認プロンプトが表示されます。

$ rm -f *
$ rm -rf *Code language: Bash (bash)

残念ながら、次の呼び出しには効果がありません。

$ rm -f myfile
$ rm -rf mysubdir
$ rm -rf mysubdir/*Code language: Bash (bash)

すべてのコマンドに影響します

また、アスタリスクと -i ファイルは、rmコマンドだけでなく、すべてのコマンドに影響します。

-i ファイルのある場所で、ls * を実行すると、lsコマンドに-iオプションをつけたことになり、ファイルのiノード番号が表示されます。

$ ls *
40906340 dummyCode language: Bash (bash)

-i ファイルのある場所で、cat *を実行すると、catコマンドには-iオプションがないので、「無効なオプション -- 'i'」と表示されます。

$ cat *
cat: 無効なオプション -- 'i'
Try 'cat --help' for more information.Code language: Bash (bash)
タイトルとURLをコピーしました