web-dev-qa-db-ja.com

なぜfindの出力をループすることが悪い習慣なのですか?

この質問は

シェルループを使用して、テキストを不適切な習慣と見なして処理するのはなぜですか?

私はこれらの構成を見る

for file in `find . -type f -name ...`; do smth with ${file}; done

そして

for dir in $(find . -type d -name ...); do smth with ${dir}; done

一部の人々がこれらの投稿にコメントして時間を割いて、なぜこの種のものを避けなければならないかを説明しているとしても、ここではほぼ毎日使用されています...
そのような投稿の数(およびそれらのコメントが単に無視されることがあるという事実)を見て、私は質問したほうがよいと思いました。

findの出力の悪い習慣とfindによって返される各ファイル名/パスに対して1つ以上のコマンドを実行する適切な方法は何ですか?

176
don_crissti

この回答は非常に大きな結果セットであり、主にパフォーマンスに関係します。たとえば、低速ネットワーク経由でファイルのリストを取得するときなどです。少量のファイル(たとえば、ローカルディスク上の数100またはおそらく1000)の場合)このうちの意味はありません。

並列処理とメモリ使用量

与えられた他の回答とは別に、分離問題などに関連して、別の問題があります

for file in `find . -type f -name ...`; do smth with ${file}; done

改行で分割される前に、バックティック内の部分を完全に評価する必要があります。これは、大量のファイルを取得した場合、さまざまなコンポーネントに存在するサイズ制限が制限される可能性があることを意味します。制限がない場合、メモリが不足する可能性があります。いずれの場合も、最初のfindを実行する前に、リスト全体がforによって出力され、smthによって解析されるまで待つ必要があります。

推奨されるUNIXの方法は、本質的に並行して実行されているパイプを操作することであり、一般に任意の巨大なバッファーを必要としません。つまり、findsmthと並行して実行し、現在のファイル名のみをRAMに保持して、smthに渡すことをお勧めします。

そのための少なくとも部分的にOKな解決策の1つは、前述のfind -exec smth。これにより、すべてのファイル名をメモリに保持する必要がなくなり、適切に並列実行されます。残念ながら、ファイルごとに1つのsmthプロセスも開始します。 smthが1つのファイルでしか機能しない場合、それが本来の方法です。

可能な限り、最適なソリューションはfind -print0 | smthsmthはSTDINでファイル名を処理できます。次に、ファイルがいくつあってもsmthプロセスは1つだけであり、2つのプロセス間で少量のバイト(組み込みパイプバッファリングが行われているものは何でも)をバッファリングするだけで済みます。もちろん、smthが標準のUnix/POSIXコマンドである場合、これはかなり現実的ではありませんが、自分で作成する場合は、アプローチになる可能性があります。

それが不可能な場合は、find -print0 | xargs -0 smthは、おそらく、より良い解決策の1つです。コメントで@ dave_thompson_085が言及しているように、xargsは、システムの制限に達したときに引数をsmthの複数の実行に分割します(デフォルトでは、128 KBの範囲またはシステムのexecによって課される制限)。 smthの1回の呼び出しに与えられるファイルの数に影響を与えるため、smthプロセスの数と初期遅延のバランスをとります。

編集:「最高」の概念を削除しました-より良いものが出現するかどうかを言うのは困難です。 ;)

11
AnoE

1つの理由は、空白が作業中にスパナをスローし、ファイル「foo bar」が「foo」および「bar」として評価されることです。

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

代わりに-execを使用すれば問題なく動作します

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
4
steve

コマンドの出力は単一の文字列ですが、ループではループするために文字列の配列が必要です。それが「機能する」理由は、シェルが空白の文字列を裏切るように分割するためです。

次に、findの特定の機能が必要でない限り、シェルが再帰的なglobパターンをそれ自体で拡張できる可能性が最も高く、重要なことに、適切な配列に拡張されることに注意してください。

バッシュの例:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

魚でも同じ:

for i in **
    echo «$i»
end

findの機能が必要な場合は、必ずNUL(find -print0 | xargs -r0イディオムなど)でのみ分割してください。

魚はNUL区切りの出力を反復できます。したがって、これは実際にはnotが不正です。

find -print0 | while read -z i
    echo «$i»
end

最後のちょっとした落とし穴として、多くのシェル(もちろんFishではありません)では、コマンド出力をループすると、ループ本体がsubshel​​lになります(つまり、後に表示される方法で変数を設定することはできません)ループは終了します)、これは決して望んでいることではありません。

2
user2394284

Findの出力をループすることは悪い習慣ではありませんこの&すべての状況で)悪い習慣は---(仮定入力はknowing(テストと確認)ではなく特定の形式です特定のフォーマット。

tldr/cbf:find | parallel stuff

1
Jan Kyu Peblik

Findによって返される各ファイル名/パスに対して1つ以上のコマンドを実行する適切な方法は何ですか?

私はMacでzshを使用しています。 fdコマンド(fzfへのパイプ)の結果を配列に取得する方法を検索しているときに、この質問を見つけました。特に、ファイル名のスペースが個別の要素として格納されることを心配する必要がなかった方法で。この配列を使用して、これらのファイル名を一度に1つずつ別のスクリプトに送信します。

私はステファンの答えに投票しようとしました。それは私が私の答えを見つけるために必要な詳細を私に与えたからです-私にとってこれを使うことでした:

array_name=(${(0)"$(fd "\.tar\.gz$|\.tgz$|\.Zip$|\.tar.bz2|\.tar$" | fzf --print0)"})

($ {(0)...})の部分が何をしているかをもっとよく理解する必要があるでしょう。多分それはIFS=$'\0'を使用できることの指摘に結びついています。クイック検索を試してみましたが、\ 0に関するものでした。私はそれをここで見つけました: 拡張に関するzshドキュメント で、それは言及しています:

14.3.1パラメータ展開フラグ

左中括弧の直後に左括弧が続く場合、対応する右括弧までの文字列がフラグのリストと見なされます。フラグを繰り返すことが意味がある場合、繰り返しは連続している必要はありません。たとえば、「(q%q%q)」は、より読みやすい「(%%qqq)」と同じ意味です。次のフラグがサポートされています。

[...]
0
拡張の結果をnullバイトに分割します。これは「ps:\0:」の省略形です。

なぜfindの出力をループすることが悪い習慣なのですか?

私は、bashスクリプトを数年使用した後でも、そのことについてアドバイスしてくれる最後の人です。 :)しかし、私がやろうとしているのは、コマンドからrcを変数に入れて変数をテストすることです。代わりに、次のタイプのifステートメントを試して使用します。

if echo “${something}” | grep '^s’ -q; then {
:
} Elif [[ $(dirname \""${file}\"") == "." ]]; then {

これがあなた/誰かを助けることを願っています。

0
Gregg