web-dev-qa-db-ja.com

findによって返されたファイル名をループする方法は?

x=$(find . -name "*.txt")
echo $x

上記のコードをBash Shellで実行すると、リストではなく、空白で区切られた複数のファイル名を含む文字列が得られます。

もちろん、それらを空白でさらに分離してリストを取得することもできますが、それを実行するためのより良い方法があると確信しています。

それでは、findコマンドの結果をループする最良の方法は何ですか?

173
Haiyuan Zhang

TL; DR:正解の答えがここにあるだけなら、おそらく私の個人的な好みであるfind . -name '*.txt' -exec process {} \;が必要です(この記事の最後を参照)。時間がある場合は、残りの部分を読んで、いくつかの異なる方法とそれらのほとんどに関する問題を確認してください。


完全な答え:

最善の方法はあなたがやりたいことによりますが、ここにいくつかの選択肢があります。サブツリー内のファイルやフォルダに名前に空白が含まれていない限り、ファイルをループすることができます。

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

余裕を持って、一時変数xを切り取ってください。

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

できればglobを使うのがずっと良いです。現在のディレクトリ内のファイルの場合は、空白文字に対して安全

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

globstarオプションを有効にすることで、このディレクトリとすべてのサブディレクトリ内で一致するすべてのファイルをグロブすることができます。

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

場合によっては、ファイル名がすでにファイル内にある場合は、readを使用する必要があります。

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readは、区切り文字を適切に設定することでfindと組み合わせて安全に使用できます。

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process $line
    done

より複雑な検索の場合は、おそらくfindを、その-execオプションまたは-print0 | xargs -0とともに使用することをお勧めします。

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findは、-execdirの代わりに-execを使用してコマンドを実行する前に各ファイルのディレクトリに移動することも、-okの代わりに-exec(または-okdirの代わりに-execdir)を使用して対話式にすることもできます。

*:技術的には、findxargsの両方(デフォルト)は、すべてのファイルを処理するのに必要な回数だけ、コマンドラインに収まる数の引数を指定してコマンドを実行します。実際には、非常に多数のファイルがあるのでなければ問題にはなりません。また、長さを超えても同じコマンドラインでそれらがすべて必要な場合は、 あなたはSOLです 別の方法を見つけてください。

299
Kevin
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

注:この方法および bmarguliesで示される(2番目の)方法は、ファイル/フォルダ名に空白を入れて使用しても安全です。

カバーされているファイル/フォルダ名の改行の - やや異例な - 大文字小文字の区別を持つためには、このようにfind-exec述語に頼る必要があります。

find . -name '*.txt' -exec echo "{}" \;

{}は見つかったアイテムのプレースホルダーで、\;-exec述語を終了するために使用されます。

そして完全を期すために別の変形を加えてみましょう - あなたはそれらの多様性のために* nixの方法を好きにならなければなりません:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

私の知る限りでは、これは印刷されたアイテムをファイル名やフォルダ名の中のどのファイルシステムでも許可されていない\0文字で分離するでしょう。 xargsはそれらを一つずつ拾います...

91
0xC0000022L

何をしても、forループを使わないでください

# Don't do this
for file in $(find . -name "*.txt")
do
    …code using "$file"
done

3つの理由

  • Forループを開始するには、findを最後まで実行する必要があります。
  • ファイル名に空白(スペース、タブ、または改行を含む)が含まれている場合、それは2つの別々の名前として扱われます。
  • 今は考えられませんが、コマンドラインバッファをオーバーランさせることができます。コマンドラインバッファが32KBを保持していて、forループが40KBのテキストを返す場合を想像してください。この最後の8KBはあなたのforループからすぐにドロップされます、そしてそれを知ることは決してないでしょう。

常にwhile read構文を使用してください。

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    …code using "$file"
done

findコマンドの実行中にループが実行されます。さらに、このコマンドは、ファイル名に空白が含まれて返された場合でも機能します。そして、あなたはあなたのコマンドラインバッファをオーバーフローさせません。

-print0は改行文字の代わりにNULLをファイル区切り文字として使用し、-d $'\0'は読み取り中に区切り文字としてNULLを使用します。

90
David W.

ファイル名にはスペースや制御文字を含めることができます。スペースは(デフォルト)bashのシェル拡張の区切り文字であり、その結果、質問からのx=$(find . -name "*.txt")は推奨されません。 findがスペースを含むファイル名を取得する場合xをループで処理すると、"the file.txt"処理用に2つの分離された文字列を取得します。これを改善するには、区切り文字(bash IFS Variable)を変更します。 \r\nに変更しますが、ファイル名に制御文字を含めることができるため、これは(完全に)安全な方法ではありません。

私の観点から、ファイルを処理するための2つの推奨される(そして安全な)パターンがあります。

1。 forループとファイル名展開を使用:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2。 find-read-whileおよびプロセス置換を使用する

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

備考

パターン1:

  1. 一致するファイルが見つからない場合、bashは検索パターン( "* .txt")を返します。そのため、「ファイルが存在しない場合は継続」という追加行が必要です。 Bash Manual、Filename Expansion を参照してください
  2. シェルオプションnullglobを使用して、この余分な行を回避できます。
  3. failglob Shellオプションが設定されていて、一致するものが見つからない場合、エラーメッセージが出力され、コマンドは実行されません。」 (上記のBashマニュアルから)
  4. シェルオプションglobstar: "設定されている場合、ファイル名展開コンテキストで使用されるパターン '**'は、すべてのファイルと0個以上のディレクトリおよびサブディレクトリに一致します。パターンの後に '/'が続く場合、ディレクトリとサブディレクトリが一致します。」 Bash Manual、Shopt Builtin を参照してください
  5. ファイル名展開の他のオプション:extglobnocaseglobdotglobおよびシェル変数GLOBIGNORE

パターン2:

  1. ファイル名には、空白、タブ、スペース、改行などを含めることができます。安全な方法でファイル名を処理するには、-print0付きのfindを使用します。 Gnu Findutilsのマンページ、安全でないファイル名の処理安全なファイル名の処理ファイル名の異常な文字 も参照してください。このトピックの詳細については、以下のDavid A. Wheelerを参照してください。

  2. Whileループで検索結果を処理するためのいくつかの可能なパターンがあります。他の人(kevin、David W.)は、パイプを使用してこれを行う方法を示しています。

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    このコードを試すと、動作しないことがわかります。files_foundは常に「true」であり、コードは常に「no files found」をエコーし​​ます。理由は次のとおりです。パイプラインの各コマンドは個別のサブシェルで実行されるため、ループ内の変更された変数(個別のサブシェル)はメインシェルスクリプトの変数を変更しません。これが、「より良い」より便利でより一般的なパターンとしてプロセス置換を使用することをお勧めする理由です。
    パイプラインにあるループに変数を設定します。なぜ消えるのか... 」(グレッグのBash FAQから)を参照してください。

追加の参照とソース:

12
Michael Brux
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
6
bmargulies

後で出力を使用したい場合は、findの出力を配列に格納できます。

array=($(find . -name "*.txt"))

それぞれの要素を新しい行に表示するために、arrayのすべての要素に対して繰り返し処理を行うforループを使用するか、printfステートメントを使用することができます。

for i in ${array[@]};do echo $i; done

または

printf '%s\n' "${array[@]}"

また使用することができます:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

これは各ファイル名を改行で表示します

リスト形式でfindの出力のみを印刷するには、以下のいずれかを使用できます。

find . -name "*.txt" -print 2>/dev/null

または

find . -name "*.txt" -print | grep -v 'Permission denied'

これはエラーメッセージを取り除き、ファイル名を改行として出力するだけです。

ファイル名で何かしたいのであれば、それを配列で保存するのがいいでしょう、そうでなければそのスペースを消費する必要はなく、あなたはfindからの出力を直接プリントすることができます。

5

ファイル名に改行が含まれていないと仮定できる場合は、次のコマンドを使用してfindの出力をBash配列に読み込むことができます。

readarray -t x < <(find . -name '*.txt')

注意:

  • -treadarrayに改行を除去させます。
  • readarrayがパイプ内にある場合は機能しません。そのため、プロセス置換が行われます。
  • readarrayはBash 4以降で利用可能です。

Bash 4.4以降では、区切り文字を指定するための-dパラメーターもサポートされています。ファイル名を区切るために改行の代わりにヌル文字を使用することは、ファイル名が改行を含むというまれなケースでもうまくいきます。

readarray -d '' x < <(find . -name '*.txt' -print0)

readarrayは、同じオプションでmapfileとして呼び出すこともできます。

参照: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

3
Seppo Enarvi

(@ Socowiの優れた速度向上を含むように更新)

それをサポートする任意の$Shell(dash/zsh/bash ...)を使って:

find . -name "*.txt" -exec $Shell -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

完了しました。


元の回答(短く、遅くなります):

find . -name "*.txt" -exec $Shell -c '
    echo "$0"
' {} \;
3
user569825

以下のように、最初にvariableに割り当てられ、IFSが新しい行に切り替えられたfindを使用します。

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

念のために、同じDATAセットに対してさらにアクションを繰り返したい場合、サーバーで非常に遅い(I/0使用率が高い)ことがわかります。

1
Paco

findによって返されるファイル名を次のような配列に入れることができます。

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

これで、配列をループして個々の項目にアクセスし、必要に応じてそれらを実行できます。

注:空白文字でも安全です。

1
Jahid

fd#3を使用して、@ phkの他の回答とコメントに基づく:
(これはまだループ内で標準入力を使用することを可能にします)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
0
Florian

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

これによりファイルが一覧表示され、属性に関する詳細が表示されます。

0
chetangb