雑多に技術メモと他色々

主に自分用な技術メモが多くなる気がする。他色々が書かれるかどうかは不明。

結局findしたファイルを消す場合はどんなコマンドを使うか

ファイルの更新時間契機なんかでfindして絞り込んだファイルを消したいケースは結構あるけど、結構ページが乱立していて無邪気にコマンド作ると遅くなったりする。
なので、結局どうすりゃいいのかを検証して、まとめてみた。
日付で絞り込む、ファイル名で検索する、といったオプションは様々な方が言及しているのでここでは割愛する。

どんなとき?

findコマンド一発で検索できる対象を、何も考えずに全部削除したい。
削除対象は数百~数千以上に膨らむ可能性がある。そんなに実行時間かけたくない。

今のところの結論

  • findの-exec rm {} \;は性能面で非推奨
  • 対応している環境ならfindの-deleteオプションで消せばいい
  • 上記オプションが対応してなくてもfindの-exec rm {} +で対応可
  • 上記も対応していない環境ならfind結果をパイプしてxargs rm

※ xargsの場合、特殊ケースのファイル名など利用には注意が必要(後述)
なんでそうなるの?は下記の検証結果から。

2020/03/25:xargs rmの記述変更

色々なケースを検証してみた

実行方式はいくつか考えられるので、パターン分けて検証。

検証環境

  • OS:Windows10 Professional
  • Shell:Git for Windows付属のGit Bash(なので、Linux環境でやったら結果変わるかも)
  • 検証方式:空ファイルをtouchで1000個作って、find経由で削除する。find時はtimeコマンドで実行時間計測 & 比較

尚、削除対象のファイルは下記のコマンドで作成。

touch fooooooooooooooooo_{1..1000}.txt

検証結果

無邪気に検索対象をワイルドカードにして-exec rmに噛ませる
$ time find *.txt -exec rm -f {} \;

real    0m37.532s
user    0m4.074s
sys     0m16.459s

一応動いてくれるのだが、通常findコマンドの第1引数は検索先のディレクトリを指定するはずなので、あまりこの形式で打たないほうがいい気がする。
※ そもそも*.txtが1件も存在しないとエラーになるし。
そしてファイル1000件で30秒オーバー。めっちゃ遅い。

カレントディレクトリを検索対象にして-exec rmに噛ませる
$ time find . -type f -exec rm -f {} \;

real    0m37.080s
user    0m3.640s
sys     0m15.960s

相変わらずファイル1000件で30秒以上かかる。ワイルドカード方式とそう変わらず遅い。
そもそもこの方式が遅いのはfind結果1件1件に対してrmコマンドが発行されるからである。
ファイルが1000件対象になると、上記方式ではrmコマンドが1000回実行される。
性能を考慮すると検索結果に対して一括でコマンド発行する方式が推奨になる。

-exec rmの実行を+で実行する方式にしてみる
$ time find . -type f -exec rm -f {} +

real    0m0.346s
user    0m0.046s
sys     0m0.249s

実行時間は比べ物にならないほど短くなる。
ただし、この方式での実行はfindした結果全件を1つのrmコマンドファイルの引数に打ち込むことになる。
ファイル数が極端に多くなったときにArgument list too longでエラーを引くリスクがある。
修正:find自身がxargsと同等の機能を備えていて、引数超過にならないように自動的に調整してくれる認識。

find結果をxargsに噛ませる
$ time find . -type f | xargs rm -f

real    0m0.376s
user    0m0.030s
sys     0m0.357s

xargsが引数をOS上限にかからないように分割してくれる(はず)なので、これもArgument list too longを引かなくて済む。
ただし、スペースやメタ文字を含むファイル名など、xargsを噛ませることで異常が発生するケースもあるので注意。
linux - How to use "xargs" properly when argument list is too long - Stack Overflow
オバフロさんは-execの方がマシだって言ってるけどそういう問題でもないような。
-exec {} +ならMAX_ARGSを意識した上で、特殊文字含むファイル名に対しても適切に操作してくれるみたいなので、オバフロさんの言う通りxargs噛ませる必要性が薄いならそちらに任せたほうが良いのだろう。

-deleteオプションで削除してみる
$ time find . -type f -delete

real    0m0.211s
user    0m0.000s
sys     0m0.202s

オプションあるやん。ということで-execやxargsでrm噛ませる以上に速いし、対応している環境なら何も考えずにこれで良いのでは。
ちなみにGit Bashだと違いが出たが、WSLのUbuntuで検証してみたところでは-exec rm +とそんなに実行時間変わらなかった。

まとめ

環境が対応していて用途に合うのであれば-delete一択で良いのではないか。
参考にしたページにも書かれているのだけど、findの-deleteオプションはググってもそれに言及した記事に当たりにくくてなぜ?ってなる。

-exec {} +方式も複数のファイルに一括コマンド発行してくれるので、これでも速度は出る。
最初この方式は引数上限意識してないのかと思ったけど、manには「xargs(1)とだいたい一緒」という記載はあるし、ARG_MAX制限回避できると言及してる人もいるし、GNUのfindのソース見てみたらxargsという名称組み込んでるみたいだし、なのでやっぱ内部で分割はやってる気がする。
少なくともWSLのUbuntu環境でMAX_ARGS上限に引っかかるファイル数を問題なく処理できるのは確認できた。

xargs方式でも大枠問題ないと思うのだが、各ブログで使えと書かれてる-iオプションは非推奨だったり、そもそもxargs(1)では使うなと書かれたり、特殊文字含むファイル名を意識したり色々注意が必要そう。
ということでxargsはもうちょい調査したほうが良さそうなのだが、一旦限界。rmじゃなくて別の操作したいときは注意+再調査。