find はディレクトリツリーを再帰的にたどり、条件に合うファイルやディレクトリをすべて返す。名前パターン、タイムスタンプ、サイズ、パーミッション——あらゆる条件で検索でき、見つけたファイルにその場でアクションを実行できる。
要は find /path -name "*.log" -type f で /path 以下の .log ファイルを全部探す。-exec で結果に即アクション、-mtime で日時絞り込み、-size でサイズ絞り、-prune でディレクトリスキップ。条件は -and、-or、-not で組み合わせ自在。
findが今も最強な理由
fdが速いのは知ってる。単純な検索なら20倍速い。でも find にしかできないことがある:
- どこにでもある。 Linux、Dockerコンテナ、CIランナー、どんな環境にも入っている。インストール不要
-execが組み込み。 パイプでxargsに渡さなくても、見つけたファイルに直接コマンドを実行できる- 複雑な条件検索。 AND、OR、NOT、カッコでのグループ化——ファイルシステム用のクエリ言語
- タイムスタンプの精密制御。 分単位で更新日時・アクセス日時・ステータス変更日時を指定
- パーミッション検索。 SUID、ワールドライタブル、特定ユーザー所有のファイルを探す
僕は普段のファイル検索には fd を使うけど、ビルドサーバーの掃除やプロダクション環境のパーミッション監査では find が必須。32blogのデプロイ時に .next のキャッシュが4GBまで膨らんでいたとき、サイズと更新日時の両方で絞り込めるのは find だけだった。
基本: 名前・タイプ・パスで探す
名前で検索
# すべてのTypeScriptファイルを検索
find . -name "*.ts"
# 大文字小文字を区別しない検索
find . -iname "*.readme"
-name はシェルのglobパターン(正規表現ではない)。パターンはファイル名全体に一致する必要がある——"*.ts" は app.ts にマッチするが app.tsx にはマッチしない。
タイプで絞り込む
# ファイルのみ(ディレクトリを除外)
find . -name "*.log" -type f
# ディレクトリのみ
find /etc -type d -name "nginx"
# シンボリックリンクのみ
find /usr/local/bin -type l
よく使う -type の値: f(ファイル)、d(ディレクトリ)、l(シンボリックリンク)。
検索開始パスを指定
# 複数ディレクトリから検索
find /var/log /tmp -name "*.log" -type f
# ルートから検索(遅い場合は -maxdepth を検討)
find / -name "nginx.conf" 2>/dev/null
更新日時で絞り込む: -mtime, -newer
時間ベースの絞り込みが find の真骨頂。ファイルには3つのタイムスタンプがある:
| オプション | タイムスタンプ | 意味 |
|---|---|---|
-mtime | mtime | 内容の更新日時 |
-atime | atime | 最終アクセス(読み取り)日時 |
-ctime | ctime | メタデータ変更日時(権限、所有者) |
+/- 記法を理解する
-mtime の後の数字は「24時間単位で何日前か」:
# 30日より前に更新されたファイル(古いファイル)
find /var/log -name "*.log" -mtime +30
# 24時間以内に更新されたファイル(新しいファイル)
find . -name "*.mdx" -mtime -1
# ちょうど7日前に更新されたファイル(168〜192時間前)
find . -mtime 7
分単位で指定する
-mtime の代わりに -mmin を使えば分単位で指定できる:
# 15分以内に変更されたファイル
find . -mmin -15
# 60分以上アクセスされていないファイル
find . -amin +60
基準ファイルより新しいファイルを探す
# deploy.logの更新日時より新しいファイル
find . -newer deploy.log
# タイムスタンプマーカーを作成し、それより新しいファイルを検索
touch -t 202603200000 /tmp/marker
find /var/log -newer /tmp/marker
サイズとパーミッションで絞る
サイズで絞る
# 100MBより大きいファイル
find / -type f -size +100M
# 1KB未満のファイル
find . -type f -size -1k
# 空ファイル
find . -type f -empty
# 空ディレクトリ
find . -type d -empty
サイズの単位: c(バイト)、k(キロバイト)、M(メガバイト)、G(ギガバイト)。
パーミッションで絞る
# 全ユーザー書き込み可能なファイル(セキュリティ監査)
find / -type f -perm -o=w 2>/dev/null
# SUIDビット付きバイナリ(セキュリティ監査)
find / -type f -perm -4000 2>/dev/null
# パーミッションが正確に644のファイル
find . -type f -perm 644
# オーナーが実行権限を持つファイル
find . -type f -perm -u=x
-perm の記法:
-perm 644— 完全一致(644でなければマッチしない)-perm -644— 指定ビットがすべてセットされている(他のビットがあってもOK)-perm /644— 指定ビットのいずれかがセットされている
所有者で絞る
# ユーザー "deploy" が所有するファイル
find /var/www -user deploy
# グループ "www-data" が所有するファイル
find /var/www -group www-data
# 所有者がいないファイル(ユーザーが削除された孤立ファイル)
find / -nouser 2>/dev/null
見つけたファイルに即アクション: -exec, -delete, -print0
ファイルを見つけるだけで終わりではない。find は結果に直接アクションを実行できる。
-exec と ;(1ファイルずつ実行)
# すべてのシェルスクリプトに実行権限を付与
find . -name "*.sh" -exec chmod +x {} \;
# 各.confファイルの詳細情報を表示
find /etc -name "*.conf" -exec ls -la {} \;
{} は現在のファイル名に置き換わる。\; はコマンドの終端(バックスラッシュでシェルからセミコロンをエスケープ)。
-exec と +(バッチ実行)
できるだけ多くのファイル名を1回のコマンドにまとめる——圧倒的に速い:
# すべてのPythonファイルの行数を一括カウント
find . -name "*.py" -exec wc -l {} +
# 全ファイルの所有者を一括変更
find /var/www -type f -exec chown deploy:www-data {} +
+ はxargsと同じように引数をまとめる。対象コマンドが複数引数を受け取れるならこちらを使う。
-delete(削除の組み込みアクション)
# .tmpファイルをすべて削除
find /tmp -name "*.tmp" -type f -delete
# 空ディレクトリを削除
find ./build -type d -empty -delete
-print0(安全なパイプ出力)
xargsにパイプする場合、スペースや特殊文字を含むファイル名に対応するため -print0 を使う:
# 安全なパイプライン: nullバイト区切り
find . -name "*.log" -print0 | xargs -0 gzip
# -print0なしだと "my log.txt" が "my" と "log.txt" に分かれて壊れる
検索の深さを制御する: -maxdepth, -prune
深さを制限する
# カレントディレクトリのみ(再帰しない)
find . -maxdepth 1 -name "*.txt"
# カレント + 1階層下まで
find . -maxdepth 2 -type f -name "*.json"
# 最初の階層をスキップ(サブディレクトリからのみ検索)
find . -mindepth 2 -name "*.ts"
ディレクトリをスキップする(-prune)
-prune はディレクトリツリーまるごとスキップする——パフォーマンスに直結:
# .tsファイルを探すが node_modules はスキップ
find . -path "*/node_modules" -prune -o -name "*.ts" -print
# 複数ディレクトリをスキップ
find . \( -path "./.git" -o -path "./node_modules" -o -path "./.next" \) -prune -o -name "*.ts" -print
パターンは -path "スキップしたいディレクトリ" -prune -o <実際の条件> -print。-o は「または」——findがディレクトリをpruneするか、マッチングを続行するか。
32blogのようなNext.jsプロジェクトで .next、node_modules、.git をスキップすると、検索が12秒から1秒未満に短縮される。
条件の組み合わせ: AND, OR, NOT
デフォルトでは複数条件はAND結合:
# .logファイルかつ10MBより大きい(暗黙のAND)
find /var/log -name "*.log" -size +10M
# 明示的なAND(結果は同じ)
find /var/log -name "*.log" -and -size +10M
OR条件
# .jpg または .png ファイル
find . -name "*.jpg" -o -name "*.png"
# アクションを付ける場合はグループ化(カッコ)が必要
find . \( -name "*.jpg" -o -name "*.png" \) -exec ls -la {} +
NOT条件
# .git以外のすべてのファイル
find . -not -path "*/.git/*"
# 短縮形: -not の代わりに !
find . ! -name "*.tmp"
複雑な条件の組み合わせ
# TypeScriptファイルで50KB超、今日更新されたもの(テスト除外)
find src/ \( -name "*.ts" -o -name "*.tsx" \) \
-size +50k -mtime 0 \
-not -path "*/__tests__/*" \
-not -name "*.test.*"
実務で使うパターン集
パターン1: 古いビルド成果物の掃除
# 7日以上前の.nextキャッシュを削除
find .next/cache -type f -mtime +7 -delete
# ~/projects以下の全プロジェクトからnode_modulesを削除
find ~/projects -maxdepth 3 -name "node_modules" -type d -prune -exec rm -rf {} +
パターン2: ディスクを食っている大きなファイルを探す
# システム全体で50MB超のファイル上位20件
find / -type f -size +50M -exec ls -lhS {} + 2>/dev/null | head -20
# /var内で90日以上アクセスされていない100MB超のファイル
find /var -type f -size +100M -atime +90 2>/dev/null
パターン3: セキュリティ監査
# SUID/SGIDビット付きバイナリを検出
find / -type f \( -perm -4000 -o -perm -2000 \) -exec ls -la {} + 2>/dev/null
# 全ユーザー書き込み可能ディレクトリ(セキュリティリスク)
find / -type d -perm -o=w -not -path "/tmp/*" -not -path "/var/tmp/*" 2>/dev/null
# 24時間以内に変更されたファイル(インシデント対応)
find / -type f -mtime 0 -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null
パターン4: Webプロジェクトのバッチ処理
# すべてのPNGをWebPに変換(-execでシェルを呼ぶ)
find public/images -name "*.png" -exec sh -c 'cwebp -q 80 "$1" -o "${1%.png}.webp"' _ {} \;
# ソースファイルの改行コードを修正
find src/ -name "*.ts" -exec dos2unix {} +
# ロケール間で重複するファイル名を検出
find content/ -name "*.mdx" -printf "%f\n" | sort | uniq -d
パターン5: Git操作
# コミット前に未追跡の大きなファイルを発見
find . -maxdepth 3 -size +5M -not -path "./.git/*" -not -path "*/node_modules/*" -type f
# .gitignoreに入れるべきファイルを検索
find . -name ".env*" -o -name "*.pem" -o -name "*.key" | grep -v ".git/"
パターン6: ログ管理
# 7日以上前のログを圧縮
find /var/log -name "*.log" -mtime +7 -exec gzip {} \;
# 90日以上前の圧縮ログを削除
find /var/log -name "*.log.gz" -mtime +90 -delete
# 直近1時間以内に更新されたログ(トラブルシュート用)
find /var/log -name "*.log" -mmin -60
find vs fd: 使い分け
fdはRust製のモダンな代替ツールで、パターンベースの検索では圧倒的に速い。僕の使い分けはこう:
| 場面 | 使うツール | 理由 |
|---|---|---|
| ファイル名のさっと検索 | fd | 約20倍速い、構文もシンプル |
| パーミッション/所有者で検索 | find | fdは非対応 |
| 複雑な日時条件 | find | -mmin、-newer 等 |
| 結果に即アクション(-exec) | find | パイプ不要で組み込み |
.gitignore を尊重 | fd | デフォルトで有効 |
| スクリプトの可搬性 | find | どの環境にもある |
| CI/CDパイプライン | find | 追加インストール不要 |
# fdで同じことをする場合
fd "\.ts$" src/ # fd: 速い、シンプル
find src/ -name "*.ts" # find: どこでも動く
# findにしかできないこと
find / -type f -perm -4000 -mtime -1 -exec ls -la {} +
fdやその他のモダンツールについてはモダンRust CLIツールを参照。
FAQ
-name と -path の違いは?
-name はファイル名(最後の要素)のみにマッチ。-path はパス全体にマッチする。ディレクトリ構造を含めて指定したいときは -path を使う: find . -path "*/config/*.json"。
グロブの代わりに正規表現を使うには?
-name の代わりに -regex を使う。デフォルトはEmacs形式の正規表現で、パス全体に対してマッチする:
# .ts または .tsx ファイルを正規表現で検索
find . -regex ".*\.\(ts\|tsx\)$"
# 拡張正規表現(POSIX ERE)のほうが書きやすい
find . -regextype posix-extended -regex ".*\.(ts|tsx)$"
「Permission denied」エラーが大量に出るのはなぜ?
読み取り権限のないディレクトリを検索しているから。2>/dev/null で標準エラーをリダイレクト: find / -name "file" 2>/dev/null。または -readable(GNU拡張)で読めないエントリをスキップ: find / -readable -name "file"。
-exec ; と -exec + の違いは?
\; はファイルごとに1回コマンドを実行。+ はできるだけ多くのファイルを1回にまとめる(xargsと同様)。+ のほうがほぼ常に高速。\; が必要なのは、シェルコマンドでファイルごとに異なる処理をする場合のみ。
findでシンボリックリンクをたどるには?
-L フラグを使う: find -L /path -name "*.conf"。デフォルトではfindはシンボリックリンクをたどらない。循環リンクがあると無限ループになるので注意。
find -delete は安全?
-print で先にテストすれば安全。find -delete は深さ優先でファイルを完全に削除する——ゴミ箱はない。必ず find <条件> -print で確認してから -print を -delete に置き換える。
ファイルの中身(内容)で検索するには?
find 自体は内容検索ができない。grepと組み合わせる:
find . -name "*.ts" -exec grep -l "useState" {} +
内容検索だけなら grep -r や rg を直接使ったほうが速い。
findの結果を件数制限するには?
find に組み込みのlimitはないが head にパイプすればいい:
find . -name "*.log" | head -5
find は head が十分な結果を得た後も実行を続ける(SIGPIPEを受け取る)。大規模検索で最初のマッチだけ欲しい場合、-quit(GNU拡張)を使う: find . -name "target" -print -quit。
まとめ
find は覚えるほど手になじむコマンド。特に使用頻度が高いパターン:
find . -name "*.ext" -type f— 基本中の基本、名前でファイルを探すfind . -mtime +30 -delete— 日時ベースのクリーンアップfind . -path "*/node_modules" -prune -o -name "*.ts" -print— ディレクトリスキップで高速化find . -name "*.log" -exec gzip {} +— パイプなしでバッチアクションfind . -name "*.txt" -print0 | xargs -0 command— xargsへの安全なパイプライン
xargsとgrepを知っていれば、find を加えてファイル処理の三種の神器が揃う。sed/awkでテキスト変換、fzfでインタラクティブ選択と組み合わせれば、ターミナルから出ずに大抵のファイルシステム操作ができる。