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 〜省略〜

複数箇所に"~/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 〜省略〜

何が悪いの?

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 〜省略〜

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

対策まとめ

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

trash-cli

効果★★★
難易度
andreafrancia/trash-cli
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-cli

Ubuntu

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

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

$ sudo apt install trash-cli

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

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

if [ -f ~/.bash_aliases ]; then source ~/.bash_aliases fi

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

$ . ~/.bashrc $ alias rm alias rm='trash-put'

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

$ rm myhoge.txt $ rm -rf hoge $ trash-list 〜省略

bash nounset オプション

効果★★★
難易度

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

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

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

#!/bin/bash -u
#!/bin/bash set -u
#!/bin/bash set -o nounset

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

$ ./test.sh ./test.sh: 行 3: TMP_USER_HOME: 未割り当ての変数です

エラー、安全性、デバッグに関する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" 〜省略〜

m4マクロで確認する

効果★★
難易度★★

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

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

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

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

define(TMP_USER_HOME, ~/tmp/xxx) rm -rf TMP_USER_HOME/*

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

$ m4 sample.m4 >sample.sh

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

$ bash sample.sh

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

$ m4 sample.m4 | bash

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

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

効果
難易度

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

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

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

仕組み

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

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

$ touch ./-i

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

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

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

効果のない場合がある

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

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

$ rm -f * $ rm -rf *

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

$ rm -f myfile $ rm -rf mysubdir $ rm -rf mysubdir/*

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

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

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

$ ls * 40906340 dummy

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

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