この質問は以下からインスピレーションを得ました。
シェルループを使用してテキストを処理するのはなぜ悪い習慣と見なされますか?
このような構造が見えます。
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とそのフラグ(およびフラグ)と一緒に安全に使用できます。-r
find
find ... -print0 | xargs -r0 ...
しかし、正しい方法はありません。理由次の理由でこのフォームを使用してください。
- 存在する必要のないGNU findutilsへの依存関係を追加します。
find
はいデザイン済み見つかったファイルに対してコマンドを実行する機能。
また、GNUではxargs
必須で-0
あり、-r
FreeBSDではxargs
必須のみが必要であり-0
(オプションはありません-r
)、一部ではxargs
まったくサポートされていません。したがって、POSIX機能(次のセクションを参照)に固執してスキップすることをお勧めします-0
。find
xargs
find
ポイント2(見つかったファイルに対してコマンドを実行する機能)については、Mike Loukidesが最もよく言ったようです。
find
仕事はファイルを見つけるのではなく、式を評価することです。はい、find
もちろんファイルを見つけることができますが、実際には副作用だけです。
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
ことです。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 {} +
(1つ以上起動することもできますsh
。)
一部のシステムでは、以下を使用できます。
find . -print0 | xargs -r0 something with
これは標準構文に比べて利点がほとんどなく、something
パイプstdin
または/dev/null
。
これを使用する理由の1つは、並列処理に-P
GNUオプションを使用することです。xargs
この問題は、プロセス置換をサポートするシェルオプションを使用してstdin
GNUを介して解決することもできます。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.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
--
(同じneedと同じように**/*
ファイルパスがで始まらないので、./
egで始まる可能性があるので注意してください。)-
ksh93
そしてbash
最終的に(再帰的なワイルドカードの高度な形式ではありませんが)のサポートが追加されましたが、**/
それでもワイルドカード修飾子がなかったので、使用は**
非常に制限されました。また、bash
4.3より前のバージョンでは、ディレクトリツリーを下るときにシンボリックリンクに従いました。
ループと同様に、$(find .)
これはファイル全体のリストをメモリに保存することを意味します。場合によっては、ファイルの操作がファイルに影響を与えたくない場合にこれが望ましい場合があります。発見するファイルの数
その他の信頼性/セキュリティに関する考慮事項
競争条件
さて、信頼性について話している場合は、ファイルを参照find
/zsh
検索し、ファイルが使用される時間の間の競合条件と標準に準拠していることを確認する必要があります(トゥクトゥゲーム)。
ディレクトリツリーを下げるときも、シンボリックリンクに従わずにTOCTOU競合なしに実行することを確認する必要があります。find
(find
少なくともGNU)openat()
正しいO_NOFOLLOW
フラグ(サポートされている場合)を使用してディレクトリを開き、各ディレクトリに対してファイル記述子を開いたままにします。 //zsh
これをしないでください。したがって、攻撃者がタイムリーにディレクトリをシンボリックリンクに置き換えることができる場合は、最終的に間違ったディレクトリに移動する可能性があります。bash
ksh
find
with がディレクトリを正しくダウンしても、例えば or のように実行されればはるかに-exec cmd {} \;
そうです。シンボリックリンクのプロパティは(そして十分なファイルが呼び出されるのを待つように競合ウィンドウが大きくなります)。-exec cmd {} +
cmd
cmd ./foo/bar
cmd ./foo/bar ./foo/bar/baz
cmd
./foo/bar
bar
find
./foo
-exec {} +
find
cmd
一部の実装には、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é.txt
cєtщ.txt
もっと悪い。文字セットがUTF-8(現在の標準)のロケールでは、63 f4 74 e9 2e 74 78 74を文字にまったくマッピングできません!
find
-name
ファイル名を対応する/述部のテキストとして扱うアプリケーションです-path
(そしてより多くの類似-iname
または-regex
いくつかの実装があります)。
find
たとえば、これは複数の実装(find
GNUシステムのGNUを含む)があることを意味します。
find . -name '*.txt'
63 f4 74 e9 2e 74 78 74
UTF-8ロケールから呼び出すと、上記のファイルが見つかりません*
(0個以上と一致)。数値、バイトではない)は、文字以外の文字と一致することはできません。
LC_ALL=C find...
Cロケールは1文字あたり1バイトを意味し、すべてのバイト値は(通常は)文字にマッピングされることが保証されているため、この問題を解決できます(一部のバイト値では定義されない可能性があります)。
シェルでこれらのファイル名を繰り返すと、バイト対文字も問題になる可能性があります。これに関して、私たちは一般的に4つの主要な種類のシェルを見ることができます。
たとえば、まだマルチバイトをサポートしていない場合
dash
、1バイトは1文字にマップされます。たとえば、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 {} \;
côté.txt
たとえば、UTF-8ロケールでは、以前のISO-8859-1が失敗します。マルチバイトサポートが優先されたり、徐々に
bash
追加されたzsh
場合。これは、文字にマッピングできないバイトを文字であるかのように考慮することに置き換えられます。まだいくつかのバグがあります。特にGBKやBIG5-HKSCSなどの一般的なマルチバイト文字セットの場合(多くのマルチバイト文字には0〜127セクションの範囲の単語(ASCII文字など)が含まれているため、かなり迷惑です)。sh
FreeBSD(少なくとも11個)のようなものまたはmksh -o utf8-mode
マルチバイトをサポートしますが、UTF-8のみをサポートするものです。
出力割り込み
中断すると、解析出力がfind
別の問題を示す可能性があります。たとえば、いくつかの制限を引き起こしたり、何らかの理由で終了したりします。find -print0
find
例:
$ (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
ここではfind
CPUタイムアウトに達して中断しました。出力がバッファリングされたため(パイプに入るとき)、いくつかのチャンクが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)
+cmd
cmd
現在のファイルパスで呼び出される(通常は関数)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
解析されます。for
smth
好ましい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
実際に複数の実行に分割され、実行回数を選択できます。影響を受けるファイルに対して単一の呼び出しが提供されるため、プロセス数と初期待ち時間のバランスを見つけます。smth
exec
smth
smth
編集:「最高」という概念を削除しました。より良いものが出てくるかどうかを言うのは難しいです。 ;)
答え4
検索結果を繰り返すのは悪い習慣ではありません。悪い習慣(この場合とすべての場合)は仮説あなたの入力は代わりに特定の形式になっています。えっ(テストと確認済み)これは特定の形式です。
tldr/cbf:find | parallel stuff