Problem
for f in $(find .)
łączy dwie niezgodne rzeczy.
findwypisuje 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 -printpo 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 findpeł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 cmdma podobne problemy (tam puste miejsca, nowa linia, pojedynczy cudzysłów, podwójny cudzysłów i ukośnik odwrotny (a przy niektórych xargimplementacjach bajty nie tworzące części prawidłowych znaków) stanowią problem)
Więcej poprawnych alternatyw
Jedynym sposobem użycia forpętli na wyjściu findbyłoby użycie zshobsługi IFS=$'\0'i:
IFS=$'\0'
for f in $(find . -print0)
(wymienić -print0ze -exec printf '%s\0' {} +dla findwdroż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 somethingmoż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 stdinalbo jest potokiem, albo /dev/null.
Jednym z powodów, dla których warto skorzystać, może być skorzystanie z -Popcji GNU xargsdo przetwarzania równoległego. stdinProblem może być także pracowali GNU xargsz -awersji 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ń somethingkażdego z nich, biorąc 20 argumentów pliku.
Za pomocą zshlub bashinnym sposobem na zapętlenie wyjścia find -print0jest 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.4i powyżej może również przechowywać pliki zwrócone przez find -print0w tablicy z:
readarray -td '' files < <(find . -print0)
zshOdpowiednik (który ma tę zaletę, że zachowanie findjest stan wyjściowy):
files=(${(0)"$(find . -print0)"})
Za pomocą zshmożna przełożyć większość findwyrażeń na kombinację globowania rekurencyjnego z kwalifikatorami globu. Na przykład zapętlenie find . -name '*.txt' -type f -mtime -1był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).
ksh93i bashostatecznie 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 bashprzed 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/ zshznajduje 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( findPrzynajmniej GNU ) robi to, otwierając katalogi używając openat()odpowiednich O_NOFOLLOWflag (jeśli są obsługiwane) i pozostawiając deskryptor pliku otwarty dla każdego katalogu, zsh/ bash/ kshnie 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 findpoprawnie opuści katalog, z, -exec cmd {} \;a tym bardziej z -exec cmd {} +, po cmdwykonaniu, na przykład gdy cmd ./foo/barlub cmd ./foo/bar ./foo/bar/baz, do czasu , kiedy zostanie cmdwykorzystany ./foo/bar, atrybuty barmogą już nie spełniać kryteriów find, ale co gorsza, ./foomogły być zastąpiony przez dowiązanie symboliczne do innego miejsca (a okno wyścigu jest znacznie większe, -exec {} +gdzie findczeka na wystarczającą ilość plików, aby zadzwonić cmd).
Niektóre findimplementacje mają (jeszcze niestandardowe) -execdirpredykaty, 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 -- barz pewnymi implementacjami, stąd --), więc ./foounika się problemu zmiany na dowiązanie symboliczne. To sprawia, że korzystanie z poleceń jest rmbezpieczniejsze (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 cmdwzroś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 cmddziała na tej ścieżce, zakończy się ENAMETOOLONGbłędem.
Za -execdirpomocą ./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 ENAMETOOLONGprawdopodobieństwo wystąpienia błędu jest mniejsze.
Bajty kontra postacie
Często pomijany przy rozważaniu bezpieczeństwa, finda 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 74byłaby przeznaczona côté.txtdla aplikacji interpretującej tę nazwę pliku w ustawieniach regionalnych, w których zestaw znaków to ISO-8859-1, oraz cєtщ.txtw 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!
findto jedna z takich aplikacji, która traktuje nazwy plików jako tekst dla swoich predykatów -name/ -path(i więcej, takich jak -inamelub -regexz niektórymi implementacjami).
Oznacza to na przykład, że ma kilka findimplementacji (w tym GNU find).
find . -name '*.txt'
nie znajdzie naszego 63 f4 74 e9 2e 74 78 74pliku 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 {} \;
findz powodzeniem znajdzie pliki, których nazwa składa się z 4 znaków zakodowanych w UTF-8, ale dashzgł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ą bashlub zshgdzie 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 shFreeBSD (przynajmniej 11) lub mksh -o utf8-modeobsługujące wiele bajtów, ale tylko dla UTF-8.
Notatki
1 Dla kompletności możemy wspomnieć o zshhackerskim 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)
+cmdto 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ć $REPLYlub zwracać kilka plików w $replytablicy). Tutaj wykonujemy przetwarzanie w tej funkcji i zwracamy false, aby plik nie został wybrany.