Das Problem
for f in $(find .)
kombiniert zwei inkompatible Dinge.
find
Gibt 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 $IFS
Listenkontext 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 -print
ist einfach nicht zuverlässig nachbearbeitbar (außer mit einem verschlungenen Trick, wie hier gezeigt ).
Das bedeutet auch, dass die Shell die Ausgabe von find
vollstä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 xarg
Implementierungen sind Bytes, die nicht Teil gültiger Zeichen sind), ein Problem.)
Richtigere Alternativen
Die einzige Möglichkeit, eine for
Schleife für die Ausgabe von find
zu verwenden zsh
, ist die Verwendung von IFS=$'\0'
und:
IFS=$'\0'
for f in $(find . -print0)
(Ersetzen -print0
durch -exec printf '%s\0' {} +
für find
Implementierungen, 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 something
Sie 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 something
sind stdin
entweder das Rohr oder die /dev/null
.
Ein Grund dafür könnte sein, dass Sie die -P
Option GNU xargs
für die parallele Verarbeitung verwenden. Das stdin
Problem kann auch mit GNU umgangen werden, xargs
mit der -a
Option, dass Shells die Prozessersetzung unterstützen:
xargs -r0n 20 -P 4 -a <(find . -print0) something
Zum Beispiel, um bis zu 4 gleichzeitige Aufrufe von something
jeweils 20 Dateiargumenten auszuführen .
Mit zsh
oder bash
können Sie die Ausgabe von auf find -print0
folgende 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.4
und darüber können auch Dateien gespeichert werden, die von find -print0
in einem Array zurückgegeben wurden mit:
readarray -td '' files < <(find . -print0)
Das zsh
Äquivalent (das den Vorteil hat, den find
Ausgangsstatus beizubehalten):
files=(${(0)"$(find . -print0)"})
Mit zsh
können Sie die meisten find
Ausdrücke in eine Kombination aus rekursivem Globbing und Glob-Qualifikationsmerkmalen übersetzen. Eine Schleife find . -name '*.txt' -type f -mtime -1
wä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).
ksh93
und bash
schließ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 bash
vor 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 find
zumindest) tut dem durch die Verzeichnisse Öffnen mit openat()
mit den richtigen O_NOFOLLOW
Flags (sofern unterstützt) und eine Dateibeschreibung für jedes Verzeichnis offen zu halten, zsh
/ bash
/ ksh
tu 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 find
das Verzeichnis ordnungsgemäß heruntergefahren wird, mit -exec cmd {} \;
und noch mehr mit -exec cmd {} +
, wenn cmd
es einmal ausgeführt wird, zum Beispiel wenn cmd ./foo/bar
oder cmd ./foo/bar ./foo/bar/baz
wenn die Zeit davon cmd
Gebrauch macht ./foo/bar
, bar
erfüllen die Attribute von möglicherweise nicht mehr die Kriterien, die mit übereinstimmen find
, aber noch schlimmer ./foo
sind ersetzt durch einen Symlink zu einem anderen Ort (und das Rennfenster ist viel größer, -exec {} +
da darauf find
gewartet wird, dass genügend Dateien zum Aufrufen vorhanden sind cmd
).
Einige find
Implementierungen haben ein (noch nicht standardmäßiges) -execdir
Prä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 -- bar
bei einigen Implementierungen, daher das --
) auf, sodass das Problem ./foo
vermieden wird, in einen Symlink geändert zu werden. Das macht die Verwendung von Befehlen rm
sicherer (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 cmd
nimmt 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), cmd
schlägt jeder Systemaufruf fehl , der auf diesem Pfad ausgeführt wird ENAMETOOLONG
.
Mit -execdir
wird 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 ENAMETOOLONG
Fehler mit geringerer Wahrscheinlichkeit auftritt.
Bytes vs Zeichen
Außerdem wird bei der Betrachtung der Sicherheit find
und 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 74
würde beispielsweise côté.txt
für eine Anwendung gelten, die diesen Dateinamen in einem Gebietsschema interpretiert, in dem der Zeichensatz ISO-8859-1 lautet, und cєtщ.txt
in 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!
find
ist eine solche Anwendung, die Dateinamen als Text für ihre -name
/ -path
Prädikate betrachtet (und mehr, wie -iname
oder -regex
mit einigen Implementierungen).
Was das bedeutet, ist das zum Beispiel mit mehreren find
Implementierungen (einschließlich GNU find
).
find . -name '*.txt'
würde unsere 63 f4 74 e9 2e 74 78 74
obige 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 {} \;
find
findet erfolgreich die Dateien, deren Name aus 4 in UTF-8 codierten Zeichen besteht, gibt jedoch dash
Lä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é.txt
von früher fehl .
Solche wie bash
oder zsh
wo 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 sh
von FreeBSD (mindestens 11) oder mksh -o utf8-mode
die 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)
+cmd
ist 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 $REPLY
mehrere Dateien in einem $reply
Array ä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.