Pocket

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

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

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

経緯はこうです。

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

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

実行しました...

TMP_USER_HOMEを設定していないので、3行目の「rm -rf $TMP_USER_HOME/*」は、「rm -rf /*」となってしまいました。

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

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

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

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

操作ミスによる削除の対策

操作ミスによる削除を防止します。削除するつもりのないファイルやディレクトリを削除しないようにするための対策です。

方法効果難易度コメント
trash-cli★★★簡単に使えて、効果大
(非推奨)
"-i" ファイルを設置する
カレントディレクトリに "-i"ファイルがあると、rm -f * を実行しても、確認プロンプトが表示される。この記事の末尾で説明。

シェルスクリプトのバグの対策

開発途中のバグは避けられません。バグに早く気づくような対策、バグによって破壊的な処理をしても、その影響を限定的にする対策です。

方法効果難易度コメント
bash nounset オプション★★★未設定の変数をエラーにする
dockerコンテナで作業する★★★★★★破壊的な処理をしても、その影響を限定的にする対策
echoで確認する
m4マクロで確認する★★★★

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がインストールされます。

Ubuntu

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

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

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

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

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

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

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

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

bash nounset オプション

効果★★★
難易度

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

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

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

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

エラー、安全性、デバッグに関する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をはずします。

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

m4マクロで確認する

効果★★
難易度★★

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

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

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

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

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

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

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

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

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

効果
難易度

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

仕組み

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

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

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

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

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

効果のない場合がある

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

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

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

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

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

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

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

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