問題
for f in $(find .)
2つの互換性のないものを組み合わせます。
find
改行文字で区切られたファイルパスのリストを出力します。$(find .)
そのリストコンテキストで引用符を付けないままにしておくと呼び出されるsplit + glob演算子は、文字$IFS
(デフォルトでは改行を含むが、スペースとタブ(およびNULを含むzsh
))で文字を分割し、結果の各単語でグロビングを実行します(ただしin zsh
)(およびksh93またはpdksh派生物のブレース展開も!)
あなたがそれを作ったとしても:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
# but not ksh93)
for f in $(find .) # invoke split+glob
改行文字はファイルパス内のすべての文字と同じように有効であるため、それは依然として間違っています。の出力find -print
は単純に後処理できません(ここに示すような複雑なトリックを使用する場合を除く)。
また、シェルfind
はファイルのループを開始する前に、完全に出力を保存し、それをsplit + glob(その出力をメモリに2回保存することを意味します)する必要があることも意味します。
find . | xargs cmd
同様の問題があることに注意してください(空白、改行、一重引用符、二重引用符、およびバックスラッシュ(および一部のxarg
実装では有効な文字の一部を形成しないバイト)が問題です)
より適切な代替案
のfor
出力でループを使用する唯一の方法は、以下をサポートfind
するものを使用するzsh
ことですIFS=$'\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 {} +
(複数起動する場合があることに注意してくださいsh
)。
一部のシステムでは、次を使用できます。
find . -print0 | xargs -r0 something with
それは、標準的な構文と手段をほとんど利点があるもののsomething
さんをstdin
いずれかのパイプですか/dev/null
。
使用したい理由の1つ-P
はxargs
、並列処理にGNU のオプションを使用することです。このstdin
問題はxargs
、-a
プロセス置換をサポートするシェルのオプションを使用してGNU で回避することもできます。
xargs -r0n 20 -P 4 -a <(find . -print0) something
たとえば、something
それぞれ20個のファイル引数を取る最大4つの同時呼び出しを実行します。
zsh
かbash
、の出力をループする別の方法はfind -print0
です:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d ''
改行で区切られたレコードではなく、NULで区切られたレコードを読み取ります。
bash-4.4
また、上記で返されるファイルをfind -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
(--
as の必要性に注意してください**/*
、ファイルパスはで始まっていないため、たとえばで./
始まって-
います)。
ksh93
そしてbash
最終的にのためのサポートを追加**/
(再帰グロブのない、より進歩フォームが)が、使用可能まだないグロブ予選**
が非常に限られています。またbash
、4.3より前は、ディレクトリツリーを下るときにシンボリックリンクに従うことに注意してください。
ループオーバーと同様に$(find .)
、これはファイルのリスト全体をメモリ1に保存することも意味します。ただし、場合によっては、ファイルに対するアクションがファイルの検出に影響を与えたくない場合(最終的に検出される可能性のあるファイルをさらに追加する場合など)が望ましい場合があります。
その他の信頼性/セキュリティに関する考慮事項
レース条件
私たちは、信頼性の話をしている場合今、私たちは時間との間の競合状態を言及する必要がfind
/ zsh
ファイルを見つけ、それを基準にし、それが使用されている時間(満たしていることを確認しTOCTOUレースを)。
ディレクトリツリーを下るときでも、シンボリックリンクをたどらないようにし、TOCTOUレースなしでそれを行う必要があります。find
(find
少なくともGNU )はopenat()
、適切なO_NOFOLLOW
フラグ(サポートされている場合)を使用してディレクトリを開き、各ディレクトリのファイル記述子を開いたままにして、zsh
/ bash
/ ksh
しないでそれを行います。そのため、攻撃者が適切なタイミングでディレクトリをシンボリックリンクに置き換えることができた場合、間違ったディレクトリに降りることができます。
場合でもfind
して、適切にディレクトリを降りない-exec cmd {} \;
となおさらで-exec cmd {} +
一度、cmd
として例えば、実行されないcmd ./foo/bar
かcmd ./foo/bar ./foo/bar/baz
、時間でcmd
の使用を作る./foo/bar
の属性はbar
、もはやマッチした基準を満たすことfind
が、さらに悪化し、./foo
あったかもしれません他の場所へのシンボリックリンクに置き換えられます(そして呼び出しウィンドウに十分なファイル-exec {} +
があるのをfind
待つ場所で、レースウィンドウがずっと大きくなりますcmd
)。
一部のfind
実装には-execdir
、2番目の問題を軽減するための(非標準の)述語があります。
と:
find . -execdir cmd -- {} \;
find
chdir()
sを実行する前に、ファイルの親ディレクトリに移動します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ライクシステムでは、ファイル名はバイトのシーケンスであるという事実です(ファイルパスのバイト値は0で、ほとんどのシステムでは( ASCIIベースのものは、今のところまれなEBCDICベースのものを無視します)0x2fはパス区切り文字です)。
これらのバイトをテキストと見なすかどうかを決定するのはアプリケーション次第です。そして、一般的には行われますが、一般的に、バイトから文字への変換は、環境に基づいてユーザーのロケールに基づいて行われます。
つまり、特定のファイル名はロケールに応じて異なるテキスト表現を持つ場合があります。たとえば、バイトシーケンス63 f4 74 e9 2e 74 78 74
はcôté.txt
、文字セットがISO-8859-1 cєtщ.txt
であるロケール、および文字セットがIS0-8859-5 であるロケールでそのファイル名を解釈するアプリケーション用です。
悪い。文字セットがUTF-8(今日の標準)であるロケールでは、63 f4 74 e9 2e 74 78 74は単に文字にマッピングできませんでした!
find
そのためのテキストとしてファイル名を考えてそのようなアプリケーションです-name
/ -path
(のような、より、述語-iname
または-regex
いくつかの実装では)。
つまり、たとえば、いくつかのfind
実装(GNUを含むfind
)を使用するということです。
find . -name '*.txt'
(バイトではなく0個以上の文字と一致63 f4 74 e9 2e 74 78 74
する)UTF-8ロケールで呼び出された場合、上記のファイルはそれらの非文字*
と一致しなかったため、見つかりません。
LC_ALL=C find...
Cロケールは文字ごとに1バイトを意味し、(一般的に)すべてのバイト値が文字にマップされることを保証するので(一部のバイト値については未定義の場合もありますが)、この問題を回避します。
シェルからこれらのファイル名をループする場合、そのバイトと文字も問題になる可能性があります。この点に関して、主に4つの主な種類のシェルがあります。
のようなまだマルチバイトを認識しないものdash
。それらの場合、バイトは文字にマッピングされます。たとえば、UTF-8ではcôté
4文字ですが、6バイトです。UTF-8が文字セットであるロケールでは、
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
UTF-8でエンコードされた4文字で構成される名前のファイルは正常に見つかりますが、dash
4〜24の範囲の長さを報告します。
yash
: 反対。文字のみを扱います。入力はすべて内部的に文字に変換されます。最も一貫性のあるシェルになりますが、任意のバイトシーケンス(有効な文字に変換されないバイトシーケンス)に対処できないことも意味します。Cロケールでも、0x7fを超えるバイト値には対応できません。
find . -exec yash -c 'echo "$1"' sh {} \;
UTF-8ロケールではcôté.txt
、たとえば以前のISO-8859-1では失敗します。
以下のようなこれらbash
またはzsh
マルチバイトサポートところは徐々に追加されました。それらは、文字にマップできないバイトを、文字であるかのように考えることにフォールバックします。特にGBKやBIG5-HKSCSのようなあまり一般的ではないマルチバイト文字セットにはいくつかのバグがあります(マルチバイト文字の多くは0から127の範囲のバイトを含んでいるので非常に厄介です) )。
sh
FreeBSD(少なくとも11)のようなもの、またはmksh -o utf8-mode
マルチバイトをサポートするがUTF-8のみをサポートするもの。
ノート
1完全を期すために、zsh
リスト全体をメモリに保存せずに再帰的なグロビングを使用してファイルをループするハッキング方法に言及できます。
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
は、cmd
現在のファイルパスを使用して(通常は関数)を呼び出すglob修飾子です$REPLY
。この関数はtrueまたはfalseを返し、ファイルを選択するかどうかを決定します(また$REPLY
、$reply
配列内の複数のファイルを変更または返すこともあります)。ここでは、その関数で処理を行い、ファイルが選択されないようにfalseを返します。