Problem
for f in $(find .)
łączy dwie niezgodne rzeczy.
find
wypisuje listę ścieżek plików rozdzielonych znakami nowej linii. Podczas gdy operator split + glob, który jest wywoływany, gdy pozostawiasz go bez $(find .)
cudzysłowu w kontekście tej listy, dzieli go na znaki $IFS
(domyślnie obejmuje znak nowej linii, ale także spację i tabulator (i NUL w zsh
)) i wykonuje globowanie dla każdego wynikowego słowa (z wyjątkiem in zsh
) (a nawet nawias klamrowy w pochodnych ksh93 lub pdksh!).
Nawet jeśli to zrobisz:
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
To nadal źle, ponieważ znak nowego wiersza jest tak samo ważny jak każdy na ścieżce pliku. Wynik działania find -print
po prostu nie jest niezawodny po przetworzeniu (z wyjątkiem użycia skomplikowanej sztuczki, jak pokazano tutaj ).
Oznacza to również, że powłoka musi w find
pełni zapisać dane wyjściowe , a następnie podzielić je + glob (co oznacza przechowywanie tego wyniku po raz drugi w pamięci), zanim zacznie się pętla nad plikami.
Zauważ, że find . | xargs cmd
ma podobne problemy (tam puste miejsca, nowa linia, pojedynczy cudzysłów, podwójny cudzysłów i ukośnik odwrotny (a przy niektórych xarg
implementacjach bajty nie tworzące części prawidłowych znaków) stanowią problem)
Więcej poprawnych alternatyw
Jedynym sposobem użycia for
pętli na wyjściu find
byłoby użycie zsh
obsługi IFS=$'\0'
i:
IFS=$'\0'
for f in $(find . -print0)
(wymienić -print0
ze -exec printf '%s\0' {} +
dla find
wdrożeń, które nie obsługują niestandardowe (ale dość powszechne w dzisiejszych czasach) -print0
).
Tutaj poprawnym i przenośnym sposobem jest użycie -exec
:
find . -exec something with {} \;
Lub jeśli something
może przyjąć więcej niż jeden argument:
find . -exec something with {} +
Jeśli potrzebujesz tej listy plików do obsługi przez powłokę:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(uwaga: może rozpocząć się więcej niż jeden sh
).
W niektórych systemach możesz użyć:
find . -print0 | xargs -r0 something with
chociaż ma to niewielką przewagę nad standardową składnią i oznacza something
, że stdin
albo jest potokiem, albo /dev/null
.
Jednym z powodów, dla których warto skorzystać, może być skorzystanie z -P
opcji GNU xargs
do przetwarzania równoległego. stdin
Problem może być także pracowali GNU xargs
z -a
wersji z powłok nośnych zmiany procesu:
xargs -r0n 20 -P 4 -a <(find . -print0) something
na przykład, aby uruchomić do 4 jednoczesnych wywołań something
każdego z nich, biorąc 20 argumentów pliku.
Za pomocą zsh
lub bash
innym sposobem na zapętlenie wyjścia find -print0
jest użycie:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d ''
czyta rekordy rozdzielane znakiem NUL zamiast rekordów rozdzielanych znakiem nowej linii.
bash-4.4
i powyżej może również przechowywać pliki zwrócone przez find -print0
w tablicy z:
readarray -td '' files < <(find . -print0)
zsh
Odpowiednik (który ma tę zaletę, że zachowanie find
jest stan wyjściowy):
files=(${(0)"$(find . -print0)"})
Za pomocą zsh
można przełożyć większość find
wyrażeń na kombinację globowania rekurencyjnego z kwalifikatorami globu. Na przykład zapętlenie find . -name '*.txt' -type f -mtime -1
byłoby:
for file (./**/*.txt(ND.m-1)) cmd $file
Lub
for file (**/*.txt(ND.m-1)) cmd -- $file
(uwaga na to, że --
tak jak w przypadku **/*
, ścieżki plików nie zaczynają się od ./
, więc -
na przykład mogą zaczynać się od).
ksh93
i bash
ostatecznie dodał wsparcie dla **/
(choć nie bardziej zaawansowanych form rekurencyjnego globowania), ale wciąż nie kwalifikatory globu, co sprawia, że użycie **
tam jest bardzo ograniczone. Uważaj również, aby bash
przed wersją 4.3 po zejściu z drzewa katalogów następowały dowiązania symboliczne.
Podobnie jak w przypadku zapętlania $(find .)
, oznacza to również przechowywanie całej listy plików w pamięci 1 . Może to być pożądane, jednak w niektórych przypadkach, gdy nie chcesz, aby twoje działania na plikach miały wpływ na wyszukiwanie plików (np. Gdy dodasz więcej plików, które same mogą zostać znalezione).
Inne kwestie dotyczące niezawodności / bezpieczeństwa
Warunki wyścigu
Teraz, jeśli mówimy o niezawodności, musimy wspomnieć o warunkach wyścigu między czasem find
/ zsh
znajduje plik i sprawdza, czy spełnia on kryteria i czas, w którym jest używany ( wyścig TOCTOU ).
Nawet schodząc z drzewa katalogów, należy uważać, aby nie podążać za dowiązaniami symbolicznymi i robić to bez wyścigu TOCTOU. find
( find
Przynajmniej GNU ) robi to, otwierając katalogi używając openat()
odpowiednich O_NOFOLLOW
flag (jeśli są obsługiwane) i pozostawiając deskryptor pliku otwarty dla każdego katalogu, zsh
/ bash
/ ksh
nie rób tego. Wobec tego, gdy osoba atakująca może w odpowiednim czasie zastąpić katalog dowiązaniem symbolicznym, możesz zejść do niewłaściwego katalogu.
Nawet jeśli find
poprawnie opuści katalog, z, -exec cmd {} \;
a tym bardziej z -exec cmd {} +
, po cmd
wykonaniu, na przykład gdy cmd ./foo/bar
lub cmd ./foo/bar ./foo/bar/baz
, do czasu , kiedy zostanie cmd
wykorzystany ./foo/bar
, atrybuty bar
mogą już nie spełniać kryteriów find
, ale co gorsza, ./foo
mogły być zastąpiony przez dowiązanie symboliczne do innego miejsca (a okno wyścigu jest znacznie większe, -exec {} +
gdzie find
czeka na wystarczającą ilość plików, aby zadzwonić cmd
).
Niektóre find
implementacje mają (jeszcze niestandardowe) -execdir
predykaty, aby złagodzić drugi problem.
Z:
find . -execdir cmd -- {} \;
find
chdir()
s do katalogu nadrzędnego pliku przed uruchomieniem cmd
. Zamiast wywoływać cmd -- ./foo/bar
, wywołuje cmd -- ./bar
( cmd -- bar
z pewnymi implementacjami, stąd --
), więc ./foo
unika się problemu zmiany na dowiązanie symboliczne. To sprawia, że korzystanie z poleceń jest rm
bezpieczniejsze (nadal może usunąć inny plik, ale nie plik z innego katalogu), ale nie polecenia, które mogą modyfikować pliki, chyba że zostały zaprojektowane tak, aby nie podążały za dowiązaniami symbolicznymi.
-execdir cmd -- {} +
czasami też działa, ale z kilkoma implementacjami, w tym z niektórymi wersjami GNU find
, jest to równoważne z -execdir cmd -- {} \;
.
-execdir
ma również tę zaletę, że omija niektóre problemy związane ze zbyt głębokimi drzewami katalogów.
W:
find . -exec cmd {} \;
rozmiar podanej ścieżki cmd
wzrośnie wraz z głębokością katalogu, w którym znajduje się plik. Jeśli rozmiar ten wzrośnie PATH_MAX
((np. 4k w systemie Linux), wówczas każde wywołanie systemowe, które cmd
działa na tej ścieżce, zakończy się ENAMETOOLONG
błędem.
Za -execdir
pomocą ./
przekazywana jest tylko nazwa pliku (ewentualnie z prefiksem ) cmd
. Same nazwy plików w większości systemów plików mają znacznie niższy limit ( NAME_MAX
) niż PATH_MAX
, więc ENAMETOOLONG
prawdopodobieństwo wystąpienia błędu jest mniejsze.
Bajty kontra postacie
Często pomijany przy rozważaniu bezpieczeństwa, find
a bardziej ogólnie przy obsłudze nazw plików w ogóle, jest fakt, że w większości systemów uniksowych nazwy plików są ciągami bajtów (dowolna wartość bajtu oprócz 0 w ścieżce pliku i w większości systemów ( Te oparte na ASCII, na razie zignorujemy te rzadkie oparte na EBCDIC) 0x2f to separator ścieżki).
Aplikacje muszą zdecydować, czy chcą traktować te bajty jako tekst. I zazwyczaj tak jest, ale generalnie tłumaczenie z bajtów na znaki odbywa się na podstawie ustawień regionalnych użytkownika i środowiska.
Oznacza to, że dana nazwa pliku może mieć różną reprezentację tekstu w zależności od ustawień regionalnych. Na przykład sekwencja bajtów 63 f4 74 e9 2e 74 78 74
byłaby przeznaczona côté.txt
dla aplikacji interpretującej tę nazwę pliku w ustawieniach regionalnych, w których zestaw znaków to ISO-8859-1, oraz cєtщ.txt
w ustawieniach regionalnych, w których zestaw znaków to IS0-8859-5.
Gorzej. W lokalizacji, w której zestaw znaków to UTF-8 (obecnie norma), 63 f4 74 e9 2e 74 78 74 po prostu nie można było przypisać do postaci!
find
to jedna z takich aplikacji, która traktuje nazwy plików jako tekst dla swoich predykatów -name
/ -path
(i więcej, takich jak -iname
lub -regex
z niektórymi implementacjami).
Oznacza to na przykład, że ma kilka find
implementacji (w tym GNU find
).
find . -name '*.txt'
nie znajdzie naszego 63 f4 74 e9 2e 74 78 74
pliku powyżej, gdy zostanie wywołany w ustawieniach regionalnych UTF-8, ponieważ *
(który pasuje do 0 lub więcej znaków , a nie bajtów) nie może pasować do tych znaków innych niż znaki.
LC_ALL=C find...
obejdzie problem, ponieważ ustawienia regionalne C sugerują jeden bajt na znak i (ogólnie) gwarantują, że wszystkie wartości bajtów zostaną odwzorowane na znak (aczkolwiek być może niezdefiniowane dla niektórych wartości bajtów).
Teraz, gdy chodzi o zapętlanie tych nazw plików z powłoki, ten bajt vs znak może również stać się problemem. Zazwyczaj widzimy 4 główne rodzaje powłok w tym zakresie:
Te, które wciąż nie są świadome wielu bajtów dash
. Dla nich bajt odwzorowuje postać. Na przykład w UTF-8 côté
ma 4 znaki, ale 6 bajtów. W lokalizacji, w której UTF-8 jest zestawem znaków, w
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
z powodzeniem znajdzie pliki, których nazwa składa się z 4 znaków zakodowanych w UTF-8, ale dash
zgłosi długości od 4 do 24.
yash
: przeciwieństwo. Zajmuje się tylko postaciami . Wszystkie dane wejściowe są wewnętrznie tłumaczone na znaki. Tworzy najbardziej spójną powłokę, ale oznacza również, że nie radzi sobie z dowolnymi sekwencjami bajtów (tymi, które nie tłumaczą się na poprawne znaki). Nawet w ustawieniach regionalnych C nie radzi sobie z wartościami bajtów powyżej 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
w ustawieniach regionalnych UTF-8 zawiedzie na przykład nasz wcześniejszy ISO-8859-1 côté.txt
.
Te lubią bash
lub zsh
gdzie stopniowo dodawana jest obsługa wielu bajtów. Powrócą do rozważania bajtów, których nie można zmapować na znaki tak, jakby były postaciami. Nadal mają kilka błędów tu i tam, zwłaszcza z mniej popularnymi wielobajtowymi zestawami znaków, takimi jak GBK lub BIG5-HKSCS (te są dość paskudne, ponieważ wiele ich znaków wielobajtowych zawiera bajty z zakresu 0-127 (jak znaki ASCII) ).
Takie jak sh
FreeBSD (przynajmniej 11) lub mksh -o utf8-mode
obsługujące wiele bajtów, ale tylko dla UTF-8.
Notatki
1 Dla kompletności możemy wspomnieć o zsh
hackerskim sposobie zapętlania plików za pomocą rekurencyjnego globowania bez zapisywania całej listy w pamięci:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
to kwalifikator glob, który wywołuje cmd
(zazwyczaj funkcję) z bieżącą ścieżką pliku w $REPLY
. Funkcja zwraca wartość true lub false, aby zdecydować, czy plik powinien zostać wybrany (i może również modyfikować $REPLY
lub zwracać kilka plików w $reply
tablicy). Tutaj wykonujemy przetwarzanie w tej funkcji i zwracamy false, aby plik nie został wybrany.