web-dev-qa-db-ja.com

`find`の-execオプションについて

自分の構文を常に調べている

find . -name "FILENAME"  -exec rm {} \;

主に、私は-exec一部は機能します。ブレース、バックスラッシュ、セミコロンの意味は何ですか?その構文の他のユースケースはありますか?

65
Zsolt Szilagy

この答えは次の部分にあります。

  • -execの基本的な使用法
  • -execsh -cと組み合わせて使用​​する
  • -exec ... {} +の使用
  • -execdirの使用

-execの基本的な使用法

-execオプションは、オプションの引数を持つ外部ユーティリティを引数として取り、それを実行します。

文字列{}が指定されたコマンドのどこかに存在する場合、その各インスタンスは、現在処理されているパス名で置き換えられます(例:./some/path/FILENAME)。ほとんどのシェルでは、2つの文字{}を引用符で囲む必要はありません。

コマンドは、findが終了する場所を知るために;で終了する必要があります(後でさらにオプションがある場合があるため)。シェルから;を保護するには、\;または';'として引用する必要があります。そうしないと、シェルはfindコマンドの終わりと見なします。

例(最初の2行の終わりにある\は、行を継続するためのものです):

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'

これにより、名前がパターン-type fと一致する通常のファイル(*.txt)が、現在のディレクトリ以下のすべてに見つかります。次に、grep -qを使用して、見つかったファイルのいずれかで文字列helloが発生するかどうかをテストします(出力を生成せず、終了ステータスのみ)。文字列を含むファイルの場合、catが実行され、ファイルの内容が端末に出力されます。

-execも、-typeおよび-nameと同様に、findによって検出されたパス名に対する「テスト」のように機能します。コマンドがゼロの終了ステータス(「成功」を意味する)を返す場合、findコマンドの次の部分が考慮されます。それ以外の場合、findコマンドは次のパス名で続行します。これは、上記の例で文字列helloを含むファイルを検索するために使用されますが、他のすべてのファイルは無視します。

上記の例は、-execの最も一般的な2つの使用例を示しています。

  1. 検索をさらに制限するためのテストとして。
  2. 見つかったパス名に対して何らかのアクションを実行するには(通常、findコマンドの最後に)(必須ではありません)。

-execsh -cと組み合わせて使用​​する

-execが実行できるコマンドは、オプションの引数を持つ外部ユーティリティに制限されています。ビルトインシェル、関数、条件、パイプライン、リダイレクトなどを-execで直接使用することは、sh -c子シェルのようなものでラップしない限り不可能です。

bash機能が必要な場合は、bash -cの代わりにsh -cを使用してください。

sh -cは、コマンドラインで指定されたスクリプトを使用して/bin/shを実行し、その後にそのスクリプトへのオプションのコマンドライン引数を続けます。

findなしでsh -cを単独で使用する簡単な例:

sh -c 'echo  "You gave me $1, thanks!"' sh "apples"

これにより、子シェルスクリプトに2つの引数が渡されます。これらは、スクリプトが使用するために$0および$1に配置されます。

  1. 文字列sh。これは、スクリプト内で$0として使用できます。内部シェルがエラーメッセージを出力した場合は、この文字列をプレフィックスとして付けます。

  2. 引数applesはスクリプト内で$1として使用でき、さらに引数があった場合、$2$3などとして使用できます。また、リスト"$@"で利用可能($0の一部ではない"$@"を除く)。

これは-execと組み合わせて使用​​すると便利です。findで検出されたパス名に作用する任意の複雑なスクリプトを作成できるためです。

例:特定のファイル名サフィックスを持つすべての通常のファイルを検索し、そのファイル名サフィックスを他のサフィックスに変更します。サフィックスは変数に保持されます。

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$from" "$to" {} ';'

内部スクリプト内では、$1は文字列text$2は文字列txt$3はパス名findが見つかりました。パラメータ拡張${3%.$1}は、パス名を受け取り、サフィックス.textを削除します。

または、dirname/basenameを使用します:

find . -type f -name "*.$from" -exec sh -c '
    mv "$3" "$(dirname "$3")/$(basename "$3" ".$1").$2"' sh "$from" "$to" {} ';'

または、内部スクリプトに変数を追加します。

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2; pathname=$3
    mv "$pathname" "$(dirname "$pathname")/$(basename "$pathname" ".$from").$to"' sh "$from" "$to" {} ';'

この最後のバリエーションでは、子シェルの変数fromtoは、外部スクリプトの同じ名前の変数とは異なることに注意してください。

上記は、findを使用して-execから任意の複雑なスクリプトを呼び出す正しい方法です。のようなループでfindを使用する

for pathname in $( find ... ); do

エラーが発生しやすく、エレガントではない(個人的な意見)。これは、ファイル名を空白で分割し、ファイル名のグロビングを呼び出します。また、ループの最初の反復を実行する前に、シェルにfindの完全な結果を展開させます。

以下も参照してください。


-exec ... {} +の使用

末尾の;+に置き換えられます。これにより、findは、見つかったパス名ごとに1回ではなく、できるだけ多くの引数(見つかったパス名)を使用して、指定されたコマンドを実行します。 これが機能するためには、文字列{}+の直前にある必要があります

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +

ここで、findは結果のパス名を収集し、できるだけ多くのパス名でcatを一度に実行します。

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +

ここでも同様に、mvは可能な限り数回実行されます。この最後の例では、coreutilsからGNU mv-tオプションをサポート)が必要です)が必要です。

-exec sh -c ... {} +を使用することも、任意の複雑なスクリプトで一連のパス名をループする効率的な方法です。

基本は-exec sh -c ... {} ';'を使用する場合と同じですが、スクリプトは引数のリストがはるかに長くなりました。これらは、スクリプト内で"$@"をループすることでループできます。

ファイル名のサフィックスを変更する前のセクションの例:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2
    shift 2  # remove the first two arguments from the list
             # because in this case these are *not* pathnames
             # given to us by find
    for pathname do  # or:  for pathname in "$@"; do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$from" "$to" {} +

-execdirの使用

-execdirもあります(ほとんどのfindバリアントによって実装されていますが、標準オプションではありません)。

これは-execのように機能しますが、指定されたシェルコマンドが現在の作業ディレクトリとして見つかったパス名のディレクトリを使用して実行され、{}にはパスなしで見つかったパス名のベース名が含まれます(ただし、 GNU findは、ベース名の前に./を付けますが、BSD findはそれを行いません)。

例:

find . -type f -name '*.txt' \
    -execdir mv {} done-texts/{}.done \;

これにより、見つかった各*.txtファイルを、ファイルが見つかった場所と同じディレクトリにある既存のdone-textsサブディレクトリに移動します。また、サフィックス.doneを追加することにより、ファイルの名前が変更されます。

ファイルの新しい名前を形成するために-execから検出されたファイルのベース名を取得する必要があるため、これは{}で行うには少しトリッキーになります。また、{}ディレクトリを適切に見つけるには、done-textsのディレクトリ名も必要です。

-execdirを使用すると、これらのようなことが簡単になります。

-execの代わりに-execdirを使用する対応する操作では、子シェルを使用する必要があります。

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "$( dirname "$name" )/done-texts/$( basename "$name" ).done"
    done' sh {} +

または、

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +
102
Kusalananda