丁寧にファイルを消させるコマンドを作りました。
経緯
ある日、サーバ上のファイルを削除しようとして rm コマンドを実行したところ、次のような出力になりました。
$ rm foobar.dat
rm: remove regular empty file 'foobar.dat'?
これは rm が、実際は rm -i のエイリアスになっています。.bashrc などで、ユーザーログイン時に自動でエイリアスを貼るようになっています。ファイル削除は慎重に行うべきなので、常にインタラクティブモードで消すように、という配慮なわけですね。
通常であれば、このプロンプトに対して y または ye または yes と入力すればファイル削除できます。ですが、これは簡単に入力を回避できてしまいます。
-f フラグを付けるとプロンプトを無視して強制的に削除できます。
$ rm -i -f kesuna.txt
$
また、yes コマンドの出力を渡すことで、y の入力を自動化できます。
$ yes | rm -i dont_delete.txt
rm: remove regular empty file 'dont_delete.txt'?
$
これでは、わざわざ rm -i のエイリアスを貼った誰かの思いが報われません。
ということで、ファイル削除前のプロンプトを簡単には省略・自動化できない rm コマンドを作りました。
成果物
tene3rm というコマンドを作りました。丁寧丁寧丁寧にファイルを削除してほしいという思いを込めています。
通常の rm コマンドには存在しない独自のプロンプトをランダムで表示するようにしました。もちろん -f オプションは実装していません。
プロンプトの例
軽くどういうプロンプトが表示されるかを説明します。
Yes / No
通常の rm と同様の単純な yes / no プロンプトを表示します。
$ ./tene3rm LICENSE
✔ tene3rm: remove file 'LICENSE'? [y/n]: y
逆パターンで確認することがあります(noじゃないとだめ)。
$ ./tene3rm LICENSE
tene3rm: DON't remove file 'LICENSE'? [y/n]: n
日本語で確認することもあります。この場合は「はい」または「は」で通ります。
$ ./tene3rm LICENSE
tene3rm: 'LICENSE' ファイルを削除しますか? [はい/いいえ]: はい
筆算
2桁の掛け算の入力を求めるプロンプトを表示します。これは途中の計算も完全に一致しないと通りません。
タイマー
数秒間のタイマーが表示されて、指定時間に合わせて Enter の入力を求めるプロンプトを表示します。猶予時間があるので、多少の誤差があっても OK です。
Captcha
ノイズや加工が加えられた Captcha 画像ファイルを生成して、その内容を読み取って入力を求めるプロンプトを表示します。
$ ./tene3rm LICENSE
tene3rm: what was written in the '/tmp/tmp.png' file?:
テトリス(Easy Mode)
Easy Mode(独自解釈)のテトリスを表示します。1行削除で100点加算なので、3行分削除した後に ESC キーで終了でファイルが削除されます。
使用したライブラリ
ユーザ入力を受け付けるプロンプトは promptui を使用しました。
TUI の実装は termbox-go を使用しています。
Captcha には captcha を使用しています。
promptui ではバリデーションも設定できるので、数値しか入力できないように制限をかけられて便利でした。
termbox-go は非常に原始的な API だけ提供されているので、UI の実装は自分でガリガリ書いてます。
Captcha は画像生成するだけなら非常に単純に呼び出せるので、サクッと導入できて良かったです。
実装
実装が大変だったのは TUI 系でした。筆算の実装と、テトリスの実装はどちらも割と手間でしたね。
テトリスについて軽く説明すると tetris.Tetris という構造体にゲームロジックを集約して実装し、Tetris 構造体のメソッド経由でゲームの状態を変更します。そして UI 側は Tetris 構造体の状態をただ画面に描画するだけ、という責務分けにしました。
以下はキー入力と、UI 描画の実装の抜粋です。キー入力待ちと画面描画は同時に処理できないため、キー入力待ちを goroutine で並列に処理しつつ、画面描画ループで main 関数が終了しないようにしています。
// promptWithTetris はEasyModeのテトリスを表示する。
func promptWithTetris(_ string) (bool, error) {
if err := termbox.Init(); err != nil {
return false, err
}
defer termbox.Close()
termbox.SetInputMode(termbox.InputEsc)
termbox.Flush()
t := tetris.NewTetris()
go waitTetrisKeyInput(t)
startTetrisGameTimer(t)
return t.ArrivedGoalScore(), nil
}
func startTetrisGameTimer(t *tetris.Tetris) {
for t.Running() {
if t.MinoCanMoveDown() {
t.MinoMoveDown()
drawTetrisScreen(t)
} else {
t.PutMino()
drawTetrisScreen(t)
if t.MinoIsOverlap() {
t.StopGame()
break
}
}
// 合計1秒のスリープとするが、割り込みで
// 次の Mino 生成を可能にするために分割してスリープする
for i := 0; i < 20; i++ {
time.Sleep(50 * time.Millisecond)
if t.ForceGenNewMino() {
t.ResetForceGenNewMino()
break
}
}
}
}
func waitTetrisKeyInput(t *tetris.Tetris) {
for t.Running() {
minoIsMoved := false
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyEsc:
t.StopGame()
case termbox.KeySpace:
t.MinoMoveBottom()
minoIsMoved = true
}
switch ev.Ch {
case 'h':
t.MinoMoveLeft()
minoIsMoved = true
case 'j':
t.MinoMoveDown()
minoIsMoved = true
case 'l':
t.MinoMoveRight()
minoIsMoved = true
}
}
if minoIsMoved {
drawTetrisScreen(t)
}
time.Sleep(50 * time.Millisecond)
}
}
感想
ここまでやれば AI を駆使しない限り簡単には自動化できない rm になったかと思います。こんなダルいコマンドを自動化するくらいなら、諦めて丁寧に手入力すると思います。
本来の「ファイル削除を慎重にやってほしい」という目的からかけ離れている気もしますが、丁寧に作業できるなら背に腹は代えられないですね。
本当の感想
こういう役に立たないくだらない物を大真面目に作ってるときが一番楽しいので、完成して発表できて満足です。
一応まだ実装したい処理がいくつかあるので、実験場代わりに変な機能を追加するかもしれないです。
例えば yes | tene3rm <file> を成功するまでループするシェルスクリプトを書いてしまえば、単純な yes / no プロンプトが表示されるまで試行する回避ができてしまうので、複数回連続でミスると一定時間次の操作を受け付けなくするとか、Ctrl-C でプロンプトを中断できないようにする、みたいなのも入れたいですね。
まとめ
以下の内容を書きました。
- 自動化・DevOps が推奨される現代で、時代を逆行するような手入力を推奨する面倒なファイル削除コマンド
tene3rmを作りました- ランダムで様々なプロンプトを表示して、ファイル削除の自動化を妨げます
- 役に立たないものを作るのは楽しい
- ご利用は自己責任で
以上です。




