HOWTO · Bash

Bash で find -exec コマンドを実行する

この記事では、`find` コマンドでファイルシステム上のパスを選び、`grep` コマンドでファイルの内容を検索する方法を説明します。

このページの内容

find はファイルシステム上のパスを選択し、grep はファイルの内容を検索します。-exec アクションは、選択した各パスを別のコマンドの引数として渡し、この2つをつなぎます。このガイドでは、再利用できる構文、内容検索、一致ごとに実行する方法とパスをまとめて処理する方法の違い、安全なファイル操作について説明します。

find -exec の仕組み

find は指定したディレクトリから検索を始め、ファイル種別、名前、更新時刻、サイズなどの条件を評価しながら、一致するパスをたどります。-exec を使うと、指定したコマンドを呼び出し、一致した各パスを {} に置き換えます。パスは引数として渡されるため、ファイル名に空白、タブ、引用符、ワイルドカード文字が含まれていても、シェルによって再解釈されません。コマンド文字列を組み立てたり、改行区切りの名前を別のコマンドに渡したりするより安全です。

検索とアクションは別々の役割を担います。-name '*.txt' のような条件でファイルを選び、grep のようなコマンドで内容を読み取ります。アクションの対象を通常ファイルだけにするには、-type f を追加します。既定では find はディレクトリも対象にするため、コマンドが受け取れる入力に合った条件を指定してください。

find -exec の基本構文

再利用できる形式は find STARTING_PATH TESTS -exec COMMAND {} \; です。STARTING_PATH は検索するディレクトリ階層を指定し、条件はアクションに渡すパスを絞り込みます。COMMAND とそのオプションで処理内容を指定し、{} は find がパスを挿入する位置を表します。対話型シェルでは、終端を \; と書き、シェルから find にセミコロンをそのまま渡します。エスケープするか引用符で囲まないと、シェルはセミコロンをコマンド区切りとして扱います。

'*.txt' のようなファイル名パターンは引用符で囲みます。こうするとシェルがパターンをそのまま渡すため、find は現在のディレクトリ内のファイルに対する展開結果ではなく、検索中の各パスで名前を照合できます。ハイフンで始まるパスは、コマンドオプションと誤認されないよう、./-archive のように開始ディレクトリを明示してください。

次の例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

set -eu
root=$(mktemp -d)
trap 'rm -rf "$root"' EXIT
mkdir -p "$root/notes"
printf 'report: first\nkeep this line\n' > "$root/notes/weekly report.txt"
printf 'draft only\n' > "$root/notes/draft.txt"
find "$root" -type f -name '*.txt' -exec grep -nH 'report' {} +

この例は独自の小さなディレクトリを作成し、テキストファイルだけを検索して、grep に一致した行番号とファイル名を表示させます。-H を指定すると、個々の呼び出しにパスが1つだけ渡される場合でもファイル名が表示されます。シェルの終了時に一時ディレクトリは削除されます。実行可能な例ですが、ここではキャプチャした出力を結果として提示していません。

find と grep でファイルの内容を検索する

/documents 以下のテキストを検索するには、find /documents -type f -name '*.txt' -exec grep -nH 'report' {} + を使います。find が対象となるテキストファイルを選び、grep が一致した各行をパスと行番号付きで表示します。一致を含むファイル名だけが必要な場合は、-nH の代わりに grep -l を使います。1つのファイルに一致する行が複数あると、行単位で表示する形式では同じパスが複数回出力されることがあります。

検索文字列は引用符で囲まれているため、1つの引数としてそのまま渡されます。正規表現を使う場合は、意図した式を指定してください。固定文字列を検索するなら、grep -F を使うと正規表現のメタ文字として解釈されません。権限やファイルの文字コードが原因で読み取れないファイルがあり、検索に失敗することもあります。スクリプトから結果を扱う際は、コマンドの診断メッセージと終了ステータスも確認してください。検索結果は読み手のディレクトリにあるファイルによって異なるため、ここではサンプルのパスを挙げていません。

; と + の使い分け

-exec COMMAND {} \; を指定すると、find は一致したパスごとにコマンドを個別に実行します。この形式は理解しやすく、パスを1つずつ処理するコマンドに適しています。シェルでエスケープしたセミコロンはアクション定義を終える記号であり、実行するコマンドには渡されません。

-exec COMMAND {} + を指定すると、find は一致した複数のパスを、システムの引数サイズ制限に収まるグループに分けて、コマンドの各呼び出しに渡します。{} は + の直前にある最後のコマンド引数として、1回だけ置きます。まとめて処理すると、grep のようなツールではプロセス起動のオーバーヘッドが減ることがよくあります。ただし、各ファイルに対する処理内容は変わらず、パスを1つだけ受け取ることを前提とするコマンドには適しません。プラス記号は find にそのまま渡されるため、シェルでエスケープする必要はありません。

一致したファイルに対してほかのコマンドを安全に実行する

古いログを削除する前の最初の確認では、アクションを -print に置き換え、表示されたパスを確認します。例:find /var/log -type f -name '*.log' -mtime +30 -print。-mtime +30 条件は、更新時刻が丸24時間の期間で30回分より前のファイルを選びます。「暦上の30日目より古い」という意味ではありません。対象を確認し、必要な権限があることを確かめてから、GNU 版やその他多くの find 実装で利用できる -delete を使います。例:find /var/log -type f -name '*.log' -mtime +30 -delete。このコマンドによる削除は取り消せないため、実行前に開始ディレクトリと検索条件を確認してください。

ダウンロード用ディレクトリから100メビバイトを超える通常ファイルを移動する場合は、まず find /downloads -type f -size +100M -print で対象を確認します。一覧に問題がなく、同じシステム上に /archive/ が存在し、適切な権限がある場合は、ファイルごとに find /downloads -type f -size +100M -exec mv -- {} /archive/ \; を実行できます。+100M のサイズ条件は find のサイズ単位を使い、次の単位に切り上げます。正確なバイト数でのしきい値が重要な場合は、実装の単位規則を確認してください。移動先に同じ名前があると上書きや名前の衝突が起こる可能性があるため、実行前に重複するベース名がないか確認します。どちらの作業でも、選択条件を絞り込み、変更前にプレビューを確認してください。