Das Problem
for f in $(find .)
kombiniert zwei inkompatible Dinge.
findGibt eine Liste der Dateipfade aus, die durch Zeilenumbrüche begrenzt sind. Während der split + glob-Operator, der aufgerufen wird, wenn Sie diesen $(find .)in diesem $IFSListenkontext nicht zitierten Operator verwenden, ihn in die Zeichen von (standardmäßig Newline, aber auch Leerzeichen und Tabulatorzeichen (und NUL in zsh)) aufteilt und mit jedem resultierenden Wort (mit Ausnahme von) ein Globen ausführt in zsh) (und sogar die Erweiterung in ksh93- oder pdksh-Derivaten abgleichen!).
Auch wenn du es schaffst:
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
Das ist immer noch falsch, da das Newline-Zeichen genauso gültig ist wie jedes andere in einem Dateipfad. Die Ausgabe von find -printist einfach nicht zuverlässig nachbearbeitbar (außer mit einem verschlungenen Trick, wie hier gezeigt ).
Das bedeutet auch, dass die Shell die Ausgabe von findvollständig speichern und dann + glob aufteilen muss (was impliziert, dass diese Ausgabe ein zweites Mal im Speicher gespeichert wird), bevor eine Schleife über die Dateien gestartet wird.
Beachten Sie, dass find . | xargs cmdähnliche Probleme auftreten (Leerzeichen, Zeilenumbrüche, einfache Anführungszeichen, doppelte Anführungszeichen und umgekehrte Schrägstriche (und bei einigen xargImplementierungen sind Bytes, die nicht Teil gültiger Zeichen sind), ein Problem.)
Richtigere Alternativen
Die einzige Möglichkeit, eine forSchleife für die Ausgabe von findzu verwenden zsh, ist die Verwendung von IFS=$'\0'und:
IFS=$'\0'
for f in $(find . -print0)
(Ersetzen -print0durch -exec printf '%s\0' {} +für findImplementierungen, die nicht den Standard unterstützen (aber heutzutage durchaus üblich) -print0).
Hier ist der richtige und tragbare Weg zu verwenden -exec:
find . -exec something with {} \;
Oder wenn somethingSie mehr als ein Argument annehmen können:
find . -exec something with {} +
Wenn Sie diese Liste von Dateien benötigen, die von einer Shell verarbeitet werden sollen:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(Vorsicht, es können mehrere gestartet werden sh).
Auf einigen Systemen können Sie Folgendes verwenden:
find . -print0 | xargs -r0 something with
aber , dass wenig Vorteil gegenüber der Standard - Syntax und Mittel somethingsind stdinentweder das Rohr oder die /dev/null.
Ein Grund dafür könnte sein, dass Sie die -POption GNU xargsfür die parallele Verarbeitung verwenden. Das stdinProblem kann auch mit GNU umgangen werden, xargsmit der -aOption, dass Shells die Prozessersetzung unterstützen:
xargs -r0n 20 -P 4 -a <(find . -print0) something
Zum Beispiel, um bis zu 4 gleichzeitige Aufrufe von somethingjeweils 20 Dateiargumenten auszuführen .
Mit zshoder bashkönnen Sie die Ausgabe von auf find -print0folgende Weise durchlaufen :
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d '' Liest NUL-getrennte Datensätze anstelle von Zeilenumbrüchen.
bash-4.4und darüber können auch Dateien gespeichert werden, die von find -print0in einem Array zurückgegeben wurden mit:
readarray -td '' files < <(find . -print0)
Das zshÄquivalent (das den Vorteil hat, den findAusgangsstatus beizubehalten):
files=(${(0)"$(find . -print0)"})
Mit zshkönnen Sie die meisten findAusdrücke in eine Kombination aus rekursivem Globbing und Glob-Qualifikationsmerkmalen übersetzen. Eine Schleife find . -name '*.txt' -type f -mtime -1wäre zum Beispiel:
for file (./**/*.txt(ND.m-1)) cmd $file
Oder
for file (**/*.txt(ND.m-1)) cmd -- $file
(Vorsicht : die Notwendigkeit , --wie bei **/*, Dateipfade beginnen , nicht ./, so kann mit beginnen -zum Beispiel).
ksh93und bashschließlich hinzugefügt Unterstützung für **/(wenn auch nicht mehr fortgeschrittene Formen des rekursiven Globbing), aber immer noch nicht die Glob-Qualifikatoren, die die Verwendung von dort **sehr begrenzt macht. Beachten Sie auch, dass bashvor 4.3 beim Abstieg in den Verzeichnisbaum Symlinks folgen.
Wie beim Loop-Over bedeutet dies auch $(find .), dass die gesamte Liste der Dateien in Speicher 1 abgelegt wird . Dies kann jedoch in einigen Fällen wünschenswert sein, wenn Sie nicht möchten, dass Ihre Aktionen für die Dateien einen Einfluss auf die Suche nach Dateien haben (z. B. wenn Sie weitere Dateien hinzufügen, die möglicherweise selbst gefunden werden).
Sonstige Überlegungen zur Zuverlässigkeit / Sicherheit
Rennbedingungen
Wenn wir jetzt von Zuverlässigkeit sprechen, müssen wir die Rennbedingungen zwischen dem Zeitpunkt find/ dem Auffindenzsh einer Datei erwähnen und prüfen , ob sie den Kriterien und dem Zeitpunkt, zu dem sie verwendet wird, entspricht ( TOCTOU-Rennen ).
Selbst wenn man einen Verzeichnisbaum herunterfährt, muss man darauf achten, dass man Symlinks nicht folgt und das ohne TOCTOU-Rennen. find(GNU findzumindest) tut dem durch die Verzeichnisse Öffnen mit openat()mit den richtigen O_NOFOLLOWFlags (sofern unterstützt) und eine Dateibeschreibung für jedes Verzeichnis offen zu halten, zsh/ bash/ kshtu das nicht. Wenn ein Angreifer also in der Lage ist, ein Verzeichnis zum richtigen Zeitpunkt durch einen Symlink zu ersetzen, kann dies dazu führen, dass das falsche Verzeichnis gefunden wird.
Selbst wenn finddas Verzeichnis ordnungsgemäß heruntergefahren wird, mit -exec cmd {} \;und noch mehr mit -exec cmd {} +, wenn cmdes einmal ausgeführt wird, zum Beispiel wenn cmd ./foo/baroder cmd ./foo/bar ./foo/bar/bazwenn die Zeit davon cmdGebrauch macht ./foo/bar, barerfüllen die Attribute von möglicherweise nicht mehr die Kriterien, die mit übereinstimmen find, aber noch schlimmer ./foosind ersetzt durch einen Symlink zu einem anderen Ort (und das Rennfenster ist viel größer, -exec {} +da darauf findgewartet wird, dass genügend Dateien zum Aufrufen vorhanden sind cmd).
Einige findImplementierungen haben ein (noch nicht standardmäßiges) -execdirPrädikat, um das zweite Problem zu lösen.
Mit:
find . -execdir cmd -- {} \;
find chdir()s in das übergeordnete Verzeichnis der Datei, bevor Sie sie ausführen cmd. Anstatt aufzurufen cmd -- ./foo/bar, ruft es cmd -- ./bar( cmd -- barbei einigen Implementierungen, daher das --) auf, sodass das Problem ./foovermieden wird, in einen Symlink geändert zu werden. Das macht die Verwendung von Befehlen rmsicherer (es könnte immer noch eine andere Datei entfernen, aber keine Datei in einem anderen Verzeichnis), aber keine Befehle, die die Dateien möglicherweise ändern, es sei denn, sie wurden so konzipiert, dass sie Symlinks nicht folgen.
-execdir cmd -- {} +manchmal funktioniert es auch, aber mit mehreren Implementierungen, einschließlich einiger Versionen von GNU find, ist es äquivalent zu -execdir cmd -- {} \;.
-execdir hat auch den Vorteil, einige der Probleme zu umgehen, die mit zu tiefen Verzeichnisbäumen verbunden sind.
Im:
find . -exec cmd {} \;
Die Größe des angegebenen Pfads cmdnimmt mit der Tiefe des Verzeichnisses zu, in dem sich die Datei befindet. Wenn diese Größe größer wird als PATH_MAX(etwa 4 KB unter Linux), cmdschlägt jeder Systemaufruf fehl , der auf diesem Pfad ausgeführt wird ENAMETOOLONG.
Mit -execdirwird nur der Dateiname (ggf. vorangestellt ./) übergeben cmd. Die Dateinamen selbst haben auf den meisten Dateisystemen eine viel niedrigere Grenze ( NAME_MAX) als PATH_MAX, sodass der ENAMETOOLONGFehler mit geringerer Wahrscheinlichkeit auftritt.
Bytes vs Zeichen
Außerdem wird bei der Betrachtung der Sicherheit findund allgemeiner beim Umgang mit Dateinamen im Allgemeinen häufig die Tatsache übersehen , dass Dateinamen auf den meisten Unix-ähnlichen Systemen Folgen von Bytes sind (jeder Byte-Wert außer 0 in einem Dateipfad und auf den meisten Systemen). ASCII-basierte, wir werden die seltenen EBCDIC-basierten vorerst ignorieren. 0x2f ist der Pfadbegrenzer.
Es liegt an den Anwendungen, zu entscheiden, ob sie diese Bytes als Text betrachten möchten. Und das tun sie im Allgemeinen, aber im Allgemeinen erfolgt die Übersetzung von Bytes in Zeichen basierend auf dem Gebietsschema des Benutzers, basierend auf der Umgebung.
Dies bedeutet, dass ein gegebener Dateiname je nach Gebietsschema unterschiedliche Textdarstellungen haben kann. Die Bytesequenz 63 f4 74 e9 2e 74 78 74würde beispielsweise côté.txtfür eine Anwendung gelten, die diesen Dateinamen in einem Gebietsschema interpretiert, in dem der Zeichensatz ISO-8859-1 lautet, und cєtщ.txtin einem Gebietsschema, in dem der Zeichensatz stattdessen IS0-8859-5 lautet.
Schlechter. In einem Gebietsschema, in dem der Zeichensatz UTF-8 ist (die heutige Norm), konnten 63 f4 74 e9 2e 74 78 74 einfach keinen Zeichen zugeordnet werden!
findist eine solche Anwendung, die Dateinamen als Text für ihre -name/ -pathPrädikate betrachtet (und mehr, wie -inameoder -regexmit einigen Implementierungen).
Was das bedeutet, ist das zum Beispiel mit mehreren findImplementierungen (einschließlich GNU find).
find . -name '*.txt'
würde unsere 63 f4 74 e9 2e 74 78 74obige Datei nicht finden, wenn sie in einem UTF-8-Gebietsschema aufgerufen wird, da *(das mit 0 oder mehr Zeichen übereinstimmt, nicht mit Bytes) nicht mit diesen Nicht-Zeichen übereinstimmen könnte.
LC_ALL=C find... würde das Problem umgehen, da das Gebietsschema C ein Byte pro Zeichen impliziert und (im Allgemeinen) garantiert, dass alle Bytewerte einem Zeichen zugeordnet sind (obwohl möglicherweise undefinierte für einige Bytewerte).
Wenn es nun darum geht, diese Dateinamen von einer Shell zu durchlaufen, kann dieses Byte gegen das Zeichen ebenfalls ein Problem werden. Wir sehen in der Regel vier Haupttypen von Muscheln in dieser Hinsicht:
Diejenigen, die noch nicht Multibyte-fähig sind, mögen dash. Für sie ist ein Byte einem Zeichen zugeordnet. In UTF-8 sind das côtébeispielsweise 4 Zeichen, aber 6 Bytes. In einem Gebietsschema, in dem UTF-8 der Zeichensatz ist, in
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
findfindet erfolgreich die Dateien, deren Name aus 4 in UTF-8 codierten Zeichen besteht, gibt jedoch dashLängen zwischen 4 und 24 aus.
yash: das Gegenteil. Es geht nur um Charaktere . Alle Eingaben werden intern in Zeichen übersetzt. Dies sorgt für die konsistenteste Shell, bedeutet aber auch, dass keine willkürlichen Byte-Sequenzen verarbeitet werden können (solche, die sich nicht in gültige Zeichen übersetzen lassen). Selbst im Gebietsschema C können keine Bytewerte über 0x7f verarbeitet werden.
find . -exec yash -c 'echo "$1"' sh {} \;
in einem UTF-8-Gebietsschema schlägt beispielsweise auf unserer ISO-8859-1 côté.txtvon früher fehl .
Solche wie bashoder zshwo die Multi-Byte-Unterstützung nach und nach hinzugefügt wurde. Diese werden auf die Berücksichtigung von Bytes zurückgreifen, die nicht wie Zeichen auf Zeichen abgebildet werden können. Hier und da gibt es immer noch ein paar Fehler, insbesondere bei weniger gebräuchlichen Multi-Byte-Zeichensätzen wie GBK oder BIG5-HKSCS (die ziemlich unangenehm sind, da viele ihrer Multi-Byte-Zeichen Bytes im Bereich 0-127 enthalten (wie die ASCII-Zeichen). ).
Diejenigen wie die shvon FreeBSD (mindestens 11) oder mksh -o utf8-modedie Multi-Bytes unterstützen, aber nur für UTF-8.
Anmerkungen
1 Der Vollständigkeit halber können wir einen Hacky-In-Weg erwähnen zsh, um Dateien mithilfe von rekursivem Globbing zu durchlaufen, ohne die gesamte Liste im Speicher zu speichern:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmdist ein Glob-Qualifizierer, der cmd(normalerweise eine Funktion) mit dem aktuellen Dateipfad in aufruft $REPLY. Die Funktion gibt true oder false zurück, um zu entscheiden, ob die Datei ausgewählt werden soll (und kann auch $REPLYmehrere Dateien in einem $replyArray ändern oder zurückgeben ). Hier führen wir die Verarbeitung in dieser Funktion durch und geben false zurück, damit die Datei nicht ausgewählt wird.