linux find再帰的検索の罠と制御

こんにちは、「MAC&Linuxサポート」の管理人です。Linuxで作業していると、「あのファイル、どこに置いたかな?」と探す場面、結構ありますよね。そんな時、findコマンドは非常に強力な味方です。

ただ、このfindコマンド、「linux find 再帰的」と調べて使い始めた方が、ちょっとした「罠」にハマることがあるんです。findコマンドは本来、指定したディレクトリ以下をすべて(再帰的に)探してくれるはずなのに、なぜか期待通りに動かない。あるいは、特定のディレクトリ(例えばnode_modulesとか.gitとか)は検索対象から除外したいのに、どうすればいいか分からない、というケースです。

この記事では、findコマンドが「再帰的に動かない」と感じる最大の理由と、その再帰動作を自由に制御する方法(例えば検索する深さの指定や、特定のファイル名だけを探す方法)について、私の経験も踏まえながら、分かりやすく整理していこうと思います。ファイルタイプや更新時間(mtime)といった検索条件の指定、さらにはgrepを使ったファイル内容の検索との違いにも触れていきますね。

  • findが「再帰的に動かない」本当の理由
  • 検索の深さを制御するmaxdepthとmindepthの使い方
  • 特定のディレクトリを検索対象から除外(prune)する方法
  • findで見つけたファイルを安全かつ高速に処理するテクニック
スポンサーリンク

linux find再帰的検索の「罠」

まず最初に、多くの方がつまずきがちな「findが再帰的に動かない!」という問題の核心に触れていきます。実はこれ、findコマンド自体の問題ではなく、Linuxシェルの「ある仕様」が原因であることがほとんどなんですね。

findが再帰的しない?シェルの罠

結論から言うと、findコマンドはデフォルトで(何もしなくても)再帰的に動作します。例えば find . と実行すれば、カレントディレクトリ以下のすべてのサブディレクトリを階層の続く限り探索してくれます。

では、なぜ「再帰的に動かない」と感じるのか。最大の原因は、シェルの「グロブ展開(Glob Expansion)」という機能にあります。

例えば、カレントディレクトリ(.)以下にある全ての「.java」ファイルを探そうとして、うっかり以下のようなコマンドを実行したとします。

NGな例: find . -name .java

もし、カレントディレクトリ(サブディレクトリではなく、今いる場所)に「Main.java」というファイルが存在した場合、シェルは find コマンドが実行される前に.java の部分を「Main.java」という文字列に置き換えてしまいます。

その結果、findコマンドは

find . -name Main.java

という形で実行されてしまい、「名前が『Main.java』のファイルを探す」という意図しない動作になります。これでは、サブディレクトリにある他の.javaファイル(例:src/Util.java)は絶対に見つかりません。これが「再帰的に動かない」と感じる正体ですね。

ファイル名指定はクォートが必須

このシェルの「おせっかい」とも言えるグロブ展開を防ぐ方法はとても簡単です。パターン(ワイルドカードを含む文字列)をクォーテーションで囲むだけです。

OKな例: find . -name ".java"

または find . -name '.java'

このようにシングルクォート(’)かダブルクォート(”)で囲むことで、シェルは .java を展開せず、そのまま「.java」という文字列としてfindコマンドに渡してくれます。

findコマンドは、渡された「.java」というパターンを使って、自身の機能で再帰的にファイルを探索します。findを使うときは、「-name」や「-path」でパターンを使う場合は必ずクォートする、と覚えておくのが確実かなと思います。

maxdepthで再帰を停止する方法

findはデフォルトで再帰的ですが、逆に「再帰してほしくない」時もありますよね。例えば、「カレントディレクトリの直下にあるファイルだけ」を一覧したい場合です。

そういう時は -maxdepth 1 オプションが便利です。

# カレントディレクトリ直下(サブディレクトリは除く)の.confファイルを探す find . -maxdepth 1 -type f -name ".conf" 

-maxdepth 1 は、「探索する階層の深さを最大1(開始地点の直下)まで」に制限します。開始地点(この場合は .)がレベル1なので、その直下(例: ./file.conf)までしか見に行かず、サブディレクトリ(例: ./subdir/other.conf)の中には入っていきません。

ちなみに -maxdepth 0 は開始地点(. 自体)にしか適用しない、というかなり特殊なオプションなので、普通に使うのは -maxdepth 1 かなと思います。

mindepthで検索開始位置を制御

-maxdepth とセットでよく使われるのが -mindepth です。これは「最低でもこの深さから検索を開始する」という指定です。

例えば、「カレントディレクトリ直下(レベル2)は無視して、サブディレクトリ(レベル3)以降にあるファイルだけを探したい」といった場合に役立ちます。

# レベル3(.//)からレベル4(.///*)までにあるREADME.mdを探す

. (レベル1)
./src (レベル2)
./src/component (レベル3)
./src/component/Button (レベル4)
find . -mindepth 3 -maxdepth 4 -name "README.md" 

-mindepth 3 を指定すると、レベル1(.)とレベル2(./src など)では検索のアクション(-nameなど)が実行されなくなります。これらを組み合わせることで、特定の階層だけをピンポイントで検索対象にできるわけですね。

特定ディレクトリを再帰から除外

開発プロジェクトなどでfindを使う時、個人的に一番やっかいなのが node_modules.gitbuild フォルダの存在です。これらの中には大量のファイルがあって、検索ノイズになるだけでなく、検索時間もかなりかかってしまいます。

これらのディレクトリを検索対象から「除外」する方法は、大きく2つあります。

方法1: -prune(高速だが難解)

伝統的で、かつ最も実行効率が良い方法が -prune を使う方法です。これは、指定したディレクトリを見つけたら「その中には入らない(枝刈りする)」という動作をさせます。

# node_modules と .git を除外して、.jsファイルを探す find . ( -path './node_modules' -o -path './.git' ) -prune -o -type f -name ".js" -print 

構文がちょっと複雑ですよね…。( ... ) で条件をグループ化し、-o は「または(OR)」を意味します。 ざっくり言うと、「もしパスが ./node_modules か ./git なら、-prune(枝刈り)して終わり。そうでなければ(-o)、.jsファイルか調べて表示(-print)してね」という論理演算になっています。

この方法の最大のメリットは、node_modulesの中身を一切スキャンしないため、パフォーマンスが劇的に向上することです。

方法2: -not -path(直感的だが遅い)

もう一つの方法は、-not(または !)を使って「このパス以外」と指定する、より直感的な方法です。

# .git と node_modules の配下(/)にあるファイルを除外 find . -type f -name ".js" -not -path "./.git/" -not -path "./node_modules/" 

こちらは非常に読みやすいんですが、重大なパフォーマンス上の問題があります。 -prune と違い、この方法はfindが node_modules の中にも再帰的に入っていきます。そして、中にある何万ものファイル一つ一つに対して「このパスは ./node_modules/ ではないか?」とチェックし、違ったら除外(表示しない)する、という動作になります。

スキャン自体は実行されてしまうため、巨大なディレクトリを除外する目的では、-not -path は実質的に使えない、と私は考えています。

結論: node_modules のような巨大なディレクトリを除外する際は、構文が難しくても -prune を使うのが鉄則ですね。

linux find再帰的検索の応用

findの再帰検索をマスターしたら、次は見つけたファイルに対して何をしたいか、という「アクション」や「絞り込み」のステップに進んでみましょう。findの本当の力はここからかもしれません。

属性(タイプ)で検索対象を絞る

findはデフォルトですべて(ファイル、ディレクトリ、シンボリックリンクなど)を見つけますが、-type オプションで対象を絞り込めます。これは本当によく使いますね。

  • -type f: 通常のファイルのみ
  • -type d: ディレクトリのみ
  • -type l: シンボリックリンクのみ

例えば、「中身が空っぽのディレクトリ」を再帰的に探して削除する、なんてこともできます。

# 空のディレクトリを探して削除 find . -type d -empty -delete 

-maxdepth 1 と組み合わせて find . -maxdepth 1 -type d とすれば、ls コマンドの代わりに「カレントディレクトリ直下のディレクトリ一覧」を取得するのにも使えます。

execで検索結果を個別に実行

findで見つけたファイルに対して、別のコマンド(rmやchmodなど)を実行したい場合、-exec オプションを使います。

方法1: -exec … {} ; (1件ずつ実行)

これは、見つけたファイル1つごと(!)にコマンドを実行する、古くからある方法です。

# 7日以上前の.logファイルを、確認しながら(-i)1つずつ削除 find . -type f -name ".log" -mtime +7 -exec rm -i {} ; 
  • {}: 見つかったファイル名が入る場所(プレースホルダー)です。
  • ;: コマンドの終わりを示します(シェルに解釈されないよう </code> でエスケープが必要です)。

この方法は、もし1万個のファイルが見つかったら、rmコマンドが1万回も起動するため、非常に遅いです。

方法2: -exec … {} + (まとめて実行)

方法1の速度問題を解決するのが、末尾を + に変える方法です。これは現代のfindでは標準的に使える機能で、私もこちらを推奨します。

# 7日以上前の.logファイルを、まとめて(高速に)削除 find . -type f -name ".log" -mtime +7 -exec rm {} + 

これは、rm file1.log file2.log file3.log ... のように、可能な限り多くのファイル名を引数にまとめて、rmコマンドの実行回数を最小限(多くの場合1回)に抑えてくれます。

ファイル名に空白や特殊文字が含まれていても安全に処理できるため、xargs を使う必要がない場面では、-exec ... + が最もシンプルで安全かつ高速かなと思います。

xargsで検索結果をまとめて実行

-exec ... + が登場する前から使われてきた、伝統的な高速化テクニックが xargs コマンドとの組み合わせです。

# -print0 と -0 の組み合わせが必須 find . -type f -name "*.log" -print0 | xargs -0 rm 

ここでの最重要ポイントは、-print0-0 のペアです。

  • -print0: findがファイル名を改行(\n)ではなく、「ヌル文字(\0)」という特殊な区切り文字で出力します。
  • xargs -0: xargsが、入力の区切り文字を改行ではなく「ヌル文字(\0)」として認識します。

なぜこれが必要かというと、「My Report.txt」のように空白を含むファイル名があった場合、-print0-0 がないと「My」と「Report.txt」という2つのファイルとして誤認識されてしまうからです。これは非常に危険なため、findとxargsをパイプ(|)で繋ぐ際は、必ずこのペアを使うようにしてください。

grepでファイル内容を再帰検索

findとよく混同されるのが grep ですね。この2つは目的が明確に違います。

  • find: ファイルのメタデータ(名前、日時、サイズ、権限など)を再帰的に探します。
  • grep: ファイルの中身(内容)を再帰的に探します。

「プロジェクト内の全ファイルから “myFunction” という文字列が書かれている場所を探したい」といった場合は、grepの出番です。

# カレントディレクトリ以下で "ERROR" という文字列を含むファイルを探す grep -r "ERROR" .

文字列を含む「ファイル名だけ」を一覧表示
grep -r -l "ERROR" . 

-r オプションが「再帰的(recursive)」を意味します。findとgrepを組み合わせて、「.logファイルの中身だけをgrepする」といった高度な使い方もできますね。

代替コマンドfdやrgとの比較

ここまでfindの使い方を解説してきましたが、特に開発プロジェクトで使う場合、最近はもっとモダンで高速な代替ツールも登場しています。私も用途によって使い分けています。

find の代替: fd (fdfind)

fd はRust製の高速なfind代替ツールです。構文が fd "pattern" と非常にシンプルなうえ、デフォルトで .gitignore を読んでくれて、node_modules.git を自動的に除外してくれます。findであの複雑な -prune 構文を書かなくていいのは、本当に楽ですね。

grep の代替: rg (ripgrep)

rg もRust製の高速なgrep代替ツールです。grep -r と同じくファイル内容を再帰検索しますが、こちらもデフォルトで .gitignore を尊重して不要なディレクトリを自動除外してくれます。

システム管理やPOSIX準拠のシェルスクリプトではfindが必須ですが、普段の開発作業でサッとファイルを探したいだけなら、fdrg は非常に強力な選択肢になるかなと思います。

linux find再帰的検索の総まとめ

今回は、「linux find 再帰的」というキーワードの裏にある「シェルの罠」から、再帰動作を制御する -maxdepth-prune、そして見つけたファイルを処理する -execxargs まで、幅広く見てきました。

findコマンドはオプションが多くて難しく感じるかもしれませんが、特に重要なポイントは以下の点かなと思います。

  • ファイル名をパターン指定(*)する時は、必ずクォート(” “)で囲む
  • node_modules などを除外する時は、-not -path ではなく効率的な -prune を使う
  • 見つけたファイルを処理する時は、-exec ... +... -print0 | xargs -0高速かつ安全に実行する

findはLinuxの基本的なコマンドでありながら、非常に奥深い機能を持っています。この記事が、皆さんのファイル検索の効率アップに少しでも役立てば幸いです。

タイトルとURLをコピーしました