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


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

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

もちろん、さらに空白で区切ってリストを取得することもできますが、もっと良い方法があると確信しています。

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


3
ファイル名をループする最善の方法は、実際に何をしたいかによってかなり異なりますが、ファイル名に空白が含まれていないことを保証できない限り、これは優れた方法ではありません。では、ファイルのループ処理で何をしたいですか?
ケビン

1
バウンティについて:ここでの主なアイデアは、考えられるすべてのケース(改行のあるファイル名、問題のある文字など)をカバーする正規の回答を得ることです。次に、これらのファイル名を使用していくつかのことを行います(別のコマンドを呼び出し、名前を変更してください...)。ありがとう!
fedorqui 'SO stop harming' 2015

ファイルまたはフォルダ名には、「。txt」の後にスペースと別の文字列が続くことを忘れないでください。例:「something.txt something」または「something.txt」
Yahya Yahyaoui

varではなく配列を使用しx=( $(find . -name "*.txt") ); echo "${x[@]}"ます。次にループできますfor item in "${x[@]}"; { echo "$item"; }
Ivan

回答:


391

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

可能な場合は、グロブする方がはるかに優れています。現在のディレクトリ内のファイルの場合、空白スペースセーフ:

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

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

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こともできます使用してコマンドを実行する前に、各ファイルのディレクトリにcd -execdirの代わりに-exec、インタラクティブ行うことができます(各ファイルに対してコマンドを実行する前にプロンプト)を使用して、-ok代わりに-exec(または-okdir代わりに-execdir)。

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


4
それの価値を持つ場合のことを指摘done < filenameし、パイプでは、次の1標準入力は、(→ループの内部には、よりインタラクティブなものを)、それ以上使用することはできませんが、1つは使用することができます必要なの例で3<はなく、<かつ追加し <&3たり-u3しますread一部では、基本的には独立したファイル記述子を使用して。また、私read -d ''は同じであると信じていますが、read -d $'\0'現時点では公式ドキュメントは見つかりません。
phk 2016年

1
* .txtのfor i; 一致するファイルがない場合、機能しません。1つのエクストラテスト、たとえば[[-e $ i]]が必要
Michael Brux

2
私はこの部分で迷っています:-exec process {} \;私の推測では、それはまったく別の問題です-それは何を意味し、どのように操作するのですか?良いQ / Aやドキュメントはどこにありますか。その上に?
Alex Hall

1
@AlexHallを使用すると、いつでもマニュアルページを参照できます(man find)。この場合、-exec通知しますfindで終了し、次のコマンド、実行する;(または+)、ここで{}(場合、または処理しているファイルの名前に置き換えられます+、その条件にそれを作ったすべてのファイル、使用されています)。
ケビン

3
@phk -d ''-d $'\0'。よりも優れています。後者は長くなるだけでなく、nullバイトを含む引数を渡すこともできますが、できません。最初のnullバイトは文字列の終わりを示します。bashのでは$'a\0bc'と同じであるa$'\0'同じである$'\0abc'か、単に空の文字列''help readdelimの最初の文字は入力を終了するために使用さ''れる」とあるので、区切り文字として使用することは少しハックのようです。空の文字列の最初の文字は、常に文字列の終わりを示すnullバイトです(明示的に書き留めていない場合でも)。
ソコウィ

114

今まで何をし、使用していない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を使用します。


3
ファイル名の改行では機能しません。-exec代わりにfindを使用してください。
ユーザー不明の

2
@userunknown-そのとおりです。-execシェルをまったく使用しないため、最も安全です。ただし、ファイル名にNLが含まれることは非常にまれです。ファイル名のスペースは非常に一般的です。主なポイントは、for多くのポスターが推奨するループを使用しないことです。
David W.

1
@userunknown-ここ。これを修正したので、新しい行、タブ、その他の空白を含むファイルが処理されます。ポストの要点は、for file $(find)それに関連する問題のためにOPを使用しないように指示することです。
デビッドW.

4
-execを使用できる場合はより良い方法ですが、シェルに返す名前を本当に必要とする場合があります。たとえば、ファイル拡張子を削除したい場合です。
Ben Reser 2014年

5
あなたは使用する必要がある-rのオプションをread-r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Dairaホップウッド

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

注:このメソッド bmarguliesが示す(2番目の)メソッドは、ファイル/フォルダー名に空白を使用しても安全です。

ファイル/フォルダ名の改行の-やや珍しい-ケースもカバーするためには、次-execfindような述語に頼る必要があります:

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

{}見つかった項目のプレースホルダであり、\;終了するために使用される-exec述語。

そして、完全を期すために、もう1つのバリアントを追加しましょう。その多様性のために* nixの方法を気に入ってください。

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

これは\0、私の知る限り、ファイル名またはフォルダー名のどのファイルシステムでも許可されていない文字で印刷されたアイテムを分離するため、すべてのベースをカバーする必要があります。xargsそれからそれらを一つずつ拾います...


3
ファイル名に改行があると失敗します。
ユーザー不明の

2
@user unknown:そうです、それは私がまったく考慮しなかったケースであり、それは非常にエキゾチックです。しかし、私はそれに応じて私の答えを調整しました。
0xC0000022L 2012年

5
おそらく価値があることを指摘find -print0し、xargs -0GNU拡張や携帯ません(POSIX)引数の両方です。ただし、それらを備えたシステムでは非常に便利です。
Toby Speight 2016

1
これは、バックスラッシュを含むファイル名(read -r修正される)または空白で終わるファイル名(修正される)でも失敗しますIFS= read。したがって、BashFAQ#1が示唆while IFS= read -r filename; do ...
Charles Duffy

1
これに関するもう1つの問題は、ループの本体が同じシェルで実行されているように見えますが、そうでexitはないため、たとえば、期待どおりに動作せず、ループ本体に設定された変数がループ後に利用できなくなります。
EM0

17

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

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

1.ループとファイル名の拡張に使用:

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シェルオプションが設定されていて、一致するものが見つからない場合、エラーメッセージが出力され、コマンドは実行されません。」(上記のBashマニュアルから)
  4. シェルオプションglobstar:「設定されている場合、ファイル名展開コンテキストで使用されるパターン「**」は、すべてのファイルと0個以上のディレクトリおよびサブディレクトリに一致します。パターンの後に「/」が続く場合、ディレクトリとサブディレクトリのみが一致します。Bashマニュアル、Shopt Builtinを参照
  5. ファイル名の展開のための他のオプション:extglobnocaseglobdotglob&シェル変数GLOBIGNORE

パターン2:

  1. ファイル名は、安全な方法でファイル名を処理するために、空白、タブ、スペース、改行を、...含めることができるfindとともに-print0使用されます。ファイル名は、すべての制御文字で印刷&NULで終了します。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」であり、コードは常に「ファイルが見つかりません」とエコーします。理由は、パイプラインの各コマンドが個別のサブシェルで実行されるため、ループ内の変更された変数(個別のサブシェル)がメインシェルスクリプトの変数を変更しないためです。これが、「より良い」、より有用で、より一般的なパターンとしてプロセス置換を使用することをお勧めする理由です。パイプラインにあるループで変数を設定するを
    参照してくださいこのトピックの詳細については、なぜ表示されないのか...(GregのBash FAQから)。

その他の参考資料と出典:


8

(@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"
' {} \;

1
糖蜜のように遅い(ファイルごとにシェルを起動するため)が、これは機能する。+1
dawg

1
代わりに、\;を使用+して、できるだけ多くのファイルを単一のに渡すことができますexec。次に"$@"、シェルスクリプト内で使用して、これらすべてのパラメーターを処理します。
ソコウィ

3
このコードにはバグがあります。ループには最初の結果がありません。これ$@は通常、スクリプトの名前であるため、省略しているためです。私達はちょうど追加する必要があるdummy間に'して{}、それはすべての一致をループで処理される保証し、スクリプト名の場所を取ることができるように。
BCartolo

新しく作成したシェルの外部から他の変数が必要な場合はどうなりますか?
浄土

OTHERVAR=foo find . -na.....$OTHERVAR新しく作成されたシェル内からアクセスできるようにする必要があります。
user569825

6
# 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

3
for x in $(find ...)空白を含むファイル名の場合は中断します。and find ... | xargsを使わない限り同じ-print0-0
glenn jackman

1
find . -name "*.txt -exec process_one {} ";"代わりに使用してください。結果を収集するためにxargsを使用する必要があるのはなぜですか?
ユーザー不明。

@userunknownまあすべては何に依存しprocess_oneます。それが実際のコマンドのプレースホルダーである場合は、それが機能することを確認してください(タイプミスを修正し、後に閉じ引用符を追加する場合"*.txt)。しかしprocess_one、ユーザー定義関数の場合、コードは機能しません。
toxalot 2014年

@toxalot:はい。ただし、呼び出すスクリプトに関数を記述しても問題はありません。
ユーザー不明

4

find後で出力を次のように使用する場合は、出力を配列に格納できます。

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

ここで、各要素を新しい行に出力するには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。そうでない場合は、そのスペースを消費する必要はなく、からの出力を直接出力できます。


1
配列のループは、ファイル名にスペースが含まれていると失敗します。
EM0

この回答を削除してください。ファイル名またはディレクトリ名にスペースを使用すると機能しません。
-jww

4

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

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

注意:

  • -t原因readarray改行を取り除くために。
  • readarrayパイプ内にある場合は機能しないため、プロセスが置き換えられます。
  • readarray Bash 4以降で利用可能です。

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

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

readarraymapfile同じオプションで呼び出すこともできます。

リファレンス:https : //mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


これが一番の答えです!動作:*ファイル名にスペース*一致するファイルがない* exit結果をループするとき
EM0

動作しませんすべてが、可能なファイル名-そのために、あなたが使用する必要がありますreadarray -d '' x < <(find . -name '*.txt' -print0)
チャールズ・ダフィー

3

最初に変数に割り当てられるfindを使用し、IFSを次のように新しい行に切り替えます。

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の使用率が高い)


2

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

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

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

注:ホワイトスペースセーフです。


1
bash 4.4以降では、ループの代わりに単一のコマンドを使用できますmapfile -t -d '' array < <(find ...)。の設定IFSは必要ありませんmapfile
ソコウィ

1

fd#3を使用した@phkの他の回答とコメントに基づく
(これにより、ループ内でstdinを使用できます)

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

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

-1

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

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


-5

findの代わりにgrepを使用する場合はどうでしょうか?

ls | grep .txt$ > out.txt

これで、このファイルを読み取ることができ、ファイル名はリストの形式になります。


6
いいえ、これを行わないでください。なぜあなたはLSの出力を解析するべきではありません。これは壊れやすい、非常に壊れやすいです。
fedorqui 'SO stop harming' 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.