メイン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
効果 | ★★★ |
難易度 | ★ |
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
Code language: Bash (bash)
Ubuntu
trash-putすると、デスクトップのゴミ箱に表示されました。
aptでインストールすると、0.12.xがインストールされます。0.12.xには、trash-restoreがありません。ファイルマネージャーnautilusでゴミ箱から元に戻します。
$ sudo apt install trash-cli
Code 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
fi
Code 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 -u
Code language: Bash (bash)
#!/bin/bash
set -o nounset
Code 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.sh
Code language: Bash (bash)
bashでsample.shを実行します。
$ bash sample.sh
Code language: Bash (bash)
問題がなければ、1行で実行します。
$ m4 sample.m4 | bash
Code language: Bash (bash)
結局、1行で実行できてしまいます。開発途中の事故は防げても、修正時の事故は起こりそうですね。
(非推奨)"-i" ファイルを設置する
効果 | ? |
難易度 | ? |

この方法はおすすめできないわ
検索していたら、この内容の記事が2件ありました。rmにこんな機能があったの?と驚きましたが、実際に試して検証してみたら、シェルのアスタリスク展開を利用したトリックでした。
これはトリッキーな方法です。次のような問題があるので、おすすめしません。
仕組み
rmコマンドに -i オプションをつけると、「削除しますか?」と確認プロンプトが表示されます。
カレントディレクトリに、a.txtがあるとして、あらたに -i ファイルを作ります。
$ touch ./-i
Code 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 dummy
Code language: Bash (bash)
-i ファイルのある場所で、cat *を実行すると、catコマンドには-iオプションがないので、「無効なオプション -- 'i'」と表示されます。
$ cat *
cat: 無効なオプション -- 'i'
Try 'cat --help' for more information.
Code language: Bash (bash)