x=$(find . -name "*.txt")
echo $x
上記のコードをBash Shellで実行すると、リストではなく、空白で区切られた複数のファイル名を含む文字列が得られます。
もちろん、それらを空白でさらに分離してリストを取得することもできますが、それを実行するためのより良い方法があると確信しています。
それでは、find
コマンドの結果をループする最良の方法は何ですか?
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
)を使用して対話式にすることもできます。
*:技術的には、find
とxargs
の両方(デフォルト)は、すべてのファイルを処理するのに必要な回数だけ、コマンドラインに収まる数の引数を指定してコマンドを実行します。実際には、非常に多数のファイルがあるのでなければ問題にはなりません。また、長さを超えても同じコマンドラインでそれらがすべて必要な場合は、 あなたはSOLです 別の方法を見つけてください。
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
はそれらを一つずつ拾います...
何をしても、for
ループを使わないでください:
# Don't do this
for file in $(find . -name "*.txt")
do
…code using "$file"
done
3つの理由
find
を最後まで実行する必要があります。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を使用します。
ファイル名にはスペースや制御文字を含めることができます。スペースは(デフォルト)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:
nullglob
を使用して、この余分な行を回避できます。failglob
Shellオプションが設定されていて、一致するものが見つからない場合、エラーメッセージが出力され、コマンドは実行されません。」 (上記のBashマニュアルから)globstar
: "設定されている場合、ファイル名展開コンテキストで使用されるパターン '**'は、すべてのファイルと0個以上のディレクトリおよびサブディレクトリに一致します。パターンの後に '/'が続く場合、ディレクトリとサブディレクトリが一致します。」 Bash Manual、Shopt Builtin を参照してくださいextglob
、nocaseglob
、dotglob
およびシェル変数GLOBIGNORE
パターン2:
ファイル名には、空白、タブ、スペース、改行などを含めることができます。安全な方法でファイル名を処理するには、-print0
付きのfind
を使用します。 Gnu Findutilsのマンページ、安全でないファイル名の処理 、 安全なファイル名の処理 、 ファイル名の異常な文字 も参照してください。このトピックの詳細については、以下のDavid A. Wheelerを参照してください。
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」をエコーします。理由は次のとおりです。パイプラインの各コマンドは個別のサブシェルで実行されるため、ループ内の変更された変数(個別のサブシェル)はメインシェルスクリプトの変数を変更しません。これが、「より良い」より便利でより一般的なパターンとしてプロセス置換を使用することをお勧めする理由です。追加の参照とソース:
# 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
後で出力を使用したい場合は、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
からの出力を直接プリントすることができます。
ファイル名に改行が含まれていないと仮定できる場合は、次のコマンドを使用してfind
の出力をBash配列に読み込むことができます。
readarray -t x < <(find . -name '*.txt')
注意:
-t
はreadarray
に改行を除去させます。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
(@ 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"
' {} \;
以下のように、最初に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使用率が高い)ことがわかります。
find
によって返されるファイル名を次のような配列に入れることができます。
array=()
while IFS= read -r -d ''; do
array+=("$REPLY")
done < <(find . -name '*.txt' -print0)
これで、配列をループして個々の項目にアクセスし、必要に応じてそれらを実行できます。
注:空白文字でも安全です。
fd#3を使用して、@ phkの他の回答とコメントに基づく:
(これはまだループ内で標準入力を使用することを可能にします)
while IFS= read -r f <&3; do
echo "$f"
done 3< <(find . -iname "*filename*")
find <path> -xdev -type f -name *.txt -exec ls -l {} \;
これによりファイルが一覧表示され、属性に関する詳細が表示されます。