質問

質問

この質問は以下からインスピレーションを得ました。

シェルループを使用してテキストを処理するのはなぜ悪い習慣と見なされますか?

このような構造が見えます。

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ループの出力が悪い習慣であるのはなぜですか?返された各ファイル名/パスに対して1つ以上のコマンドを実行する正しい方法は何ですかfind

答え1

出力反復がfind悪い習慣なのはなぜですか?

簡単な答えは次のとおりです。

ファイル名には以下を含めることができます。どの特徴。

だから、ファイル名を区別するために確実に使用できる印刷可能文字はありません。


改行文字は次のとおりです。よく(不正確に)ファイル名を区別する理由は次のとおりです。以上ファイル名に改行を含めます。

ただし、任意の仮定に基づいてソフトウェアを構築すると、せいぜい例外を処理できなくなり、最悪の場合、システム制御を失う悪意のある攻撃に対して脆弱になります。したがって、これは堅牢性とセキュリティの問題です。

2つの異なる方法でソフトウェアを書くことができますが、そのうちの1つは極端なケース(異常な入力)を正しく処理しましたが、もう1つは読みやすくなった場合はこれをトレードオフとして考えることができます。 (そうではありません。私は正しいコードを好みます。)

しかし、正確で強力なコードバージョンがある場合返品読みやすく極端な場合に失敗するコードを書く理由はありません。find見つかったすべてのファイルに対してコマンドを実行する必要がある場合です。


より具体的に説明すると、UNIXまたはLinuxシステムでは、ファイル名に/a(パスコンポーネント区切り文字として使用)を除く任意の文字を含めることができ、nullバイトを含めることはできません。

したがって、ヌルバイトはただファイル名を分離する正しい方法。


GNUにはメインがfind含まれているため、-print0印刷するファイル名を区切るためにNULLバイトを使用します。 GNUfind できるxargs次の出力を処理する-0ために、GNUとそのフラグ(およびフラグ)と一緒に安全に使用できます。-rfind

find ... -print0 | xargs -r0 ...

しかし、正しい方法はありません。理由次の理由でこのフォームを使用してください。

  1. 存在する必要のないGNU findutilsへの依存関係を追加します。
  2. findはいデザイン済み見つかったファイルに対してコマンドを実行する機能。

また、GNUではxargs必須で-0あり、-rFreeBSDではxargs必須のみが必要であり-0(オプションはありません-r)、一部ではxargsまったくサポートされていません。したがって、POSIX機能(次のセクションを参照)に固執してスキップすることをお勧めします-0findxargs

findポイント2(見つかったファイルに対してコマンドを実行する機能)については、Mike Loukidesが最もよく言ったようです。

find仕事はファイルを見つけるのではなく、式を評価することです。はい、findもちろんファイルを見つけることができますが、実際には副作用だけです。

- Unix電動工具


POSIX 指定目的find

find各結果に対して1つ以上のコマンドを実行する正しい方法は何ですか?

見つかった各ファイルに対して単一のコマンドを実行するには、次のようにします。

find dirname ... -exec somecommand {} \;

見つかった各ファイルに対して複数のコマンドを順番に実行するには、最初のコマンドが成功した場合にのみ2番目のコマンドを実行するには、次のようにします。

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

複数のファイルに対して単一のコマンドを同時に実行するには、次のようにします。

find dirname ... -exec somecommand {} +

find結合するsh

使用する必要がある場合シェル出力リダイレクト、ファイル名から拡張子の削除など、コマンド内の機能にこのsh -c構成を使用できます。これについて知っておくべきことがいくつかあります。

  • いいえ{}shコードに直接挿入してください。これにより、悪意を持って作成されたファイル名から任意のコードを実行する可能性があります。そして、実際にはPOSIXは動作することを明示していません。 (次のトピックを参照してください。)

  • {}何度も使用したり、長い引数の一部として使用しないでください。 これはポータブルではありません。たとえば、次のようにしないでください。

    find ... -exec cp {} somedir/{}.bak \;

    引用するPOSIX仕様find:

    もしユーティリティ名またはディスカッション文字列に「{}」の2文字が含まれているかどうかは実装によって定義されますが、「{}」の2文字のみが含まれるわけではありません。探すこれら2つの文字を置き換えるか、変更されていない文字列を使用してください。

    ...2 文字 "{}" を含む引数が複数ある場合、動作は指定されません。

  • オプションに渡されるシェルコマンド文字列の後の引数は、-cシェルの位置引数に設定されます。から始まる$0。で始めないでください$1

    したがって、たとえば、生成されたシェルからエラーを報告するために使用される「ダミー」$0値を含めることをお勧めします。これにより、複数のファイルをシェルに渡すのとfind-sh同じ構文を使用できます。一方、省略された値は、最初に渡されたファイルがに設定されているため、含まれていないことを意味します。"$@"$0$0"$@"


各ファイルに対して単一のシェルコマンドを実行するには、次のようにします。

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

ただし、通常、見つかったすべてのファイルに対してシェルを生成しないように、シェルループでファイルを処理する方が良いパフォーマンスが得られます。

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(各位置引数はfor f do同じfor f in "$@"; doで、順番に処理されます。つまり、find名前に特殊文字があるかどうかに関係なく、見つかったすべてのファイルを使用します。)


正しい使い方findの追加例:

(注:このリストを自由に展開してください。)

答え2

質問

for f in $(find .)

両立できない二つを一つにまとめる。

find改行で区切られたファイルパスのリストを印刷します。$(find .)リストコンテキストで引用符を引用符で囲んでいないままにすると、分割+glob演算子が呼び出され、これを文字(デフォルトでは改行文字を含む、スペースとタブ(およびNUL in)を含む)に分割し、$IFSすべての項目zshでglobbing操作(除外)をを行います。結果ワードzsh(ksh93の中かっこ拡張(braceexpand以前のバージョンでそのオプションがオフになっていても)またはpdksh派生語!)。

そうしても:

IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion
              # done upon other expansions in ksh)
for f in $(find .) # invoke split+glob

改行はファイルパスのすべての文字と同じくらい有効であるため、これはまだ間違っています。のfind -print出力図からわかるように)。

これはまた、シェルが出力を完全に保存し、ファイルへfindのループを開始する前に出力を分割+グローブする必要があることを意味します(出力をメモリに再保存することを意味します)。

同様の問題があります(スペース、改行、一重引用符、二重引用符、およびバックスラッシュ(有効な文字の一部を形成しないfind . | xargs cmd特定の実装バイトを含む)もすべて問題です)。xarg

もっと正しい選択

for出力でループを使用する唯一の方法は、およびのサポートを使用するfindことです。zshIFS=$'\0'

IFS=$'\0'
for f in $(find . -print0)

-print0サポートされていない非標準実装に置き換えられます(ただし、現在は一般的です)-exec printf '%s\0' {} +)。find-print0

ここで正確で移植可能な方法は、以下を使用することです-exec

find . -exec something with {} \;

またはsomething複数のパラメータが利用可能な場合:

find . -exec something with {} +

シェルでファイルのリストを処理する必要がある場合:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(1つ以上起動することもできますsh。)

一部のシステムでは、以下を使用できます。

find . -print0 | xargs -r0 something with

これは標準構文に比べて利点がほとんどなく、somethingパイプstdinまたは/dev/null

これを使用する理由の1つは、並列処理に-PGNUオプションを使用することです。xargsこの問題は、プロセス置換をサポートするシェルオプションを使用してstdinGNUを介して解決することもできます。xargs-a

xargs -r0n 20 -P 4 -a <(find . -print0) something

somethingたとえば、それぞれ20個のファイルパラメータを使用して最大4個の同時呼び出しを実行します。

zshまたはを使用してbash出力を繰り返すもう1つの方法は次find -print0のとおりです。

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d ''改行で区切られたレコードの代わりにNULで区切られたレコードを読み取ります。

bash-4.4find -print0上記は、返されたファイルを配列に保存することもできます。

readarray -td '' files < <(find . -print0)

同等zsh(終了状態が保存されるという利点がありますfind):

files=(${(0)"$(find . -print0)"})

を使用すると、ほとんどの式を再帰ワイルドカードとワイルドカード修飾子の組み合わせに変換zshできます。findたとえば、ループは次のようfind . -name '*.txt' -type f -mtime -1になります。

for file (./**/*.txt(ND.m-1)) cmd $file

または

for file (**/*.txt(ND.m-1)) cmd -- $file

--(同じneedと同じように**/*ファイルパスがで始まらないので、./egで始まる可能性があるので注意してください。)-

ksh93そしてbash最終的に(再帰的なワイルドカードの高度な形式ではありませんが)のサポートが追加されましたが、**/それでもワイルドカード修飾子がなかったので、使用は**非常に制限されました。また、bash4.3より前のバージョンでは、ディレクトリツリーを下るときにシンボリックリンクに従いました。

ループと同様に、$(find .)これはファイル全体のリストをメモリに保存することを意味します。場合によっては、ファイルの操作がファイルに影響を与えたくない場合にこれが望ましい場合があります。発見するファイルの数

その他の信頼性/セキュリティに関する考慮事項

競争条件

さて、信頼性について話している場合は、ファイルを参照find/zsh検索し、ファイルが使用される時間の間の競合条件と標準に準拠していることを確認する必要があります(トゥクトゥゲーム)。

ディレクトリツリーを下げるときも、シンボリックリンクに従わずにTOCTOU競合なしに実行することを確認する必要があります。findfind少なくともGNU)openat()正しいO_NOFOLLOWフラグ(サポートされている場合)を使用してディレクトリを開き、各ディレクトリに対してファイル記述子を開いたままにします。 //zshこれをしないでください。したがって、攻撃者がタイムリーにディレクトリをシンボリックリンクに置き換えることができる場合は、最終的に間違ったディレクトリに移動する可能性があります。bashksh

findwith がディレクトリを正しくダウンしても、例えば or のように実行されればはるかに-exec cmd {} \;そうです。シンボリックリンクのプロパティは(そして十分なファイルが呼び出されるのを待つように競合ウィンドウが大きくなります)。-exec cmd {} +cmdcmd ./foo/barcmd ./foo/bar ./foo/bar/bazcmd./foo/barbarfind./foo-exec {} +findcmd

一部の実装には、2番目の問題を軽減するためのfind(非標準)述語があります。-execdir

そして:

find . -execdir cmd -- {} \;

find chdir()を実行する前に、ファイルの親ディレクトリに移動してくださいcmd。これは呼び出しではなく呼び出しであるcmd -- ./foo/barためcmd -- ./bar、(cmd -- bar一部の実装では呼び出しです)--シンボリックリンクに変更する問題を回避できます。./fooこれにより、rm同様のコマンドを使用する方が安全ですが、シンボリックリンクに従わないように設計されていない限り、ファイルを変更できるコマンドは使用できません。

-execdir cmd -- {} +時には動作しますが、いくつかのバージョンのGNUを含むいくつかの実装findでは-execdir cmd -- {} \;

-execdirまた、深すぎるディレクトリツリーに関連するいくつかの問題を解決する利点もあります。

存在する:

find . -exec cmd {} \;

指定されたパスのサイズは、ファイルがcmdあるディレクトリの深さに応じて増加します。サイズが大きい場合PATH_MAX(Linuxでは約4k)、そのcmdパスで実行されたすべてのシステムコールはエラーで失敗しますENAMETOOLONG

の場合、-execdirファイル名(プレフィックスを含めることができます./)のみに渡されますcmd。ファイル名自体はほとんどのファイルシステムではるかにNAME_MAX低い制限()を持つPATH_MAXため、ENAMETOOLONGこのエラーが発生する可能性はほとんどありません。

バイトと文字

さらに、セキュリティを考慮し、findファイル名をより一般的に扱うと、事実が見落とされることがよくあります。ほとんどのUnixファミリーシステムでは、ファイル名はバイトシーケンスです(ファイルパス値ではゼロ以外のすべてのバイト、ほとんどのシステムでは)。 (ASCIIベース(現在はまれなEBCDICベースを無視する))0x2fはパス区切り文字です。

これらのバイトをテキストとして処理するかどうかは、アプリケーションによって異なります。一般的にそうです。ただし、通常、バイトから文字への変換は、ユーザーのロケールと環境に応じて行われます。

これは、指定されたファイル名がロケールによって異なるテキスト表現を持つことができることを意味します。たとえば、バイトシーケンスは、文字セットISO-8859-1を持つロケールのファイル名を解釈するアプリケーション、および文字セットIS0-8859-5を持つロケールのファイル名を解釈するアプリケーションに63 f4 74 e9 2e 74 78 74適しています。côté.txtcєtщ.txt

もっと悪い。文字セットがUTF-8(現在の標準)のロケールでは、63 f4 74 e9 2e 74 78 74を文字にまったくマッピングできません!

find-nameファイル名を対応する/述部のテキストとして扱うアプリケーションです-path(そしてより多くの類似-inameまたは-regexいくつかの実装があります)。

findたとえば、これは複数の実装(findGNUシステムのGNUを含む)があることを意味します。

find . -name '*.txt'

63 f4 74 e9 2e 74 78 74UTF-8ロケールから呼び出すと、上記のファイルが見つかりません*(0個以上と一致)。数値、バイトではない)は、文字以外の文字と一致することはできません。

LC_ALL=C find...Cロケールは1文字あたり1バイトを意味し、すべてのバイト値は(通常は)文字にマッピングされることが保証されているため、この問題を解決できます(一部のバイト値では定義されない可能性があります)。

シェルでこれらのファイル名を繰り返すと、バイト対文字も問題になる可能性があります。これに関して、私たちは一般的に4つの主要な種類のシェルを見ることができます。

  1. たとえば、まだマルチバイトをサポートしていない場合dash、1バイトは1文字にマップされます。たとえば、UTF-8ではcôté4文字ですが、6バイトです。 UTF-8が文字セットであるロケールでは、

     find . -name '????' -exec dash -c '
       name=${1##*/}; echo "${#name}"' sh {} \;
    

    findUTF-8でエンコードされた4文字の名前を持つファイルは正常に検索されますが、dash長さの範囲は4〜24として報告されます。

  2. yash:その逆だ。それだけが含まれています数値。必要な入力はすべて内部的に文字に変換されます。これは、最も一貫したシェルを提供しますが、任意のバイトシーケンス(有効な文字に変換できないシーケンス)を処理できないことを意味します。 Cロケールでも0x7f以上のバイト値を処理できません。

     find . -exec yash -c 'echo "$1"' sh {} \;
    

    côté.txtたとえば、UTF-8ロケールでは、以前のISO-8859-1が失敗します。

  3. マルチバイトサポートが優先されたり、徐々にbash追加されたzsh場合。これは、文字にマッピングできないバイトを文字であるかのように考慮することに置き換えられます。まだいくつかのバグがあります。特にGBKやBIG5-HKSCSなどの一般的なマルチバイト文字セットの場合(多くのマルチバイト文字には0〜127セクションの範囲の単語(ASCII文字など)が含まれているため、かなり迷惑です)。

  4. shFreeBSD(少なくとも11個)のようなものまたはmksh -o utf8-modeマルチバイトをサポートしますが、UTF-8のみをサポートするものです。

出力割り込み

中断すると、解析出力がfind別の問題を示す可能性があります。たとえば、いくつかの制限を引き起こしたり、何らかの理由で終了したりします。find -print0find

例:

$ (ulimit -t 1; find / -type f -print0 2> /dev/null) | xargs -r0 printf 'rm -rf "%s"\n' | tail -n 2
rm -rf "/usr/lib/x86_64-linux-gnu/guile/2.2/ccache/language/ecmascript/parse.go"
rm -rf "/usr/"
zsh: cpu limit exceeded (core dumped)  ( ulimit -t 1; find / -type f -print0 2> /dev/null; ) |
zsh: done                              xargs -r0 printf 'rm -rf "%s"\n' | tail -n 2

ここではfindCPUタイムアウトに達して中断しました。出力がバッファリングされたため(パイプに入るとき)、いくつかのチャンクがstdoutとして出力され、終了したときに書き込まれた最後のチャンクの終わりは、偶然にいくつかのファイルパスの途中にありました。残念ながらfindここには。/usr/lib/x86_64-linux-gnu/guile.../usr/

xargs、EOFが続く区別されていない/usr/レコードを見て、それをに渡しましたprintf。このコマンドがrm -rf反対の場合、重大な結果が発生する可能性があります。


ノート

1完全性を期すために、zshリスト全体をメモリに保存するのではなく、再帰ワイルドカードを使用してファイルを繰り返す賢い方法に言及できます。

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdcmd現在のファイルパスで呼び出される(通常は関数)glob修飾子です$REPLY。この関数は、ファイルを選択する必要があるかどうかを判断するためにtrueまたはfalseを返します。また、$REPLY配列内の複数のファイルを変更または返すこともできます$reply。ここでは関数が処理してfalseを返すので、ファイルは選択されません。

² GNUはパターンマッチングのためにfindシステムのfnmatch()libc関数を使用するので、その関数がテキストではなくデータを処理する方法によって動作が異なります。

答え3

この回答は非常に大きな結果セットのためのものであり、主に遅いネットワークを介してファイルのリストをインポートするときなどのパフォーマンスに関連しています。少数のファイル(たとえば、ローカルディスクに100個または1000個など)の場合、ほとんどは意味がありません。

並列性とメモリ使用量

分離問題などに関連する他の回答に加えて、別の質問があります。

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

逆引用符内の部分は、改行に分割する前に最初に完全に評価する必要があります。つまり、多数のファイルを取得すると、個々のコンポーネントに存在するサイズの制限によってブロックされる可能性があります。制限がないと、どのような場合でもメモリが不足する可能性があります。リスト全体が出力され、最初のリストが実行される前にfind解析されます。forsmth

好ましいUNIXアプローチは、本質的に並列に実行され、通常はランダムに大きなバッファを必要としないパイプを使用することです。つまり、並列にfind実行してsmthファイルを引き渡している間は、現在のファイル名をRAMに保持することをお勧めしますsmth

少なくとも部分的に実現可能な解決策が前述されているfind -exec smth。すべてのファイル名をメモリに保存する必要はなく、並列にスムーズに実行されます。残念ながら、smth各ファイルのプロセスも開始されます。 1つのファイルしか処理できない場合は、そのファイルでsmthなければなりません。

最善の解決策は、可能であればSTDINがファイル名を処理find -print0 | smthできることです。smthこれにより、ファイルがどれだけ多くても、1つのsmthプロセスだけがあり、2つのプロセスの間に少数のバイトしかバッファリングできません(内部パイプバッファリングが進行するかにかかわらず)。もちろん、これがsmth標準のUnix / POSIXコマンドであれば、かなり非現実的です。しかし、自分で書くなら、これは良い方法です。

これが不可能な場合は、これがfind -print0 | xargs -0 smthより良い解決策の1つかもしれません。コメントで@dave_thompson_085が述べたように、システム制限(デフォルトでは128 KBの範囲またはシステムによって課された制限)に達すると、パラメータはxargs実際に複数の実行に分割され、実行回数を選択できます。影響を受けるファイルに対して単一の呼び出しが提供されるため、プロセス数と初期待ち時間のバランスを見つけます。smthexecsmthsmth

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

答え4

検索結果を繰り返すのは悪い習慣ではありません。悪い習慣(この場合とすべての場合)は仮説あなたの入力は代わりに特定の形式になっています。えっ(テストと確認済み)これは特定の形式です。

tldr/cbf:find | parallel stuff

関連情報