Dlaczego zapętlanie wyników wyszukiwania jest złą praktyką?

170

To pytanie jest inspirowane przez

Dlaczego używanie pętli powłoki do przetwarzania tekstu jest uważane za złą praktykę?

Widzę te konstrukty

for file in `find . -type f -name ...`; do smth with ${file}; done

i

for dir in $(find . -type d -name ...); do smth with ${dir}; done

używane tutaj prawie codziennie, nawet jeśli niektórzy ludzie poświęcają czas na komentowanie tych postów, wyjaśniając, dlaczego tego rodzaju rzeczy należy unikać ...
Widząc liczbę takich postów (i fakt, że czasami te komentarze są po prostu ignorowane) Pomyślałem, że równie dobrze mogę zadać pytanie:

Dlaczego zapętlanie findwyjścia jest złą praktyką i jaki jest właściwy sposób uruchamiania jednej lub więcej komend dla każdej nazwy / ścieżki pliku zwracanej przez find?

don_crissti
źródło
12
Myślę, że jest to coś w rodzaju „Nigdy nie analizuj danych wyjściowych!” - z pewnością możesz zrobić jedno z nich jednorazowo, ale są one szybszym hakiem niż jakością produkcji. Lub bardziej ogólnie, zdecydowanie nigdy nie bądź dogmatyczny.
Bruce Ediger,
To powinno zostać przekształcone w odpowiedź kanoniczną
Zaid
6
Ponieważ punktem znalezienia jest zapętlenie tego, co znajdzie.
OrangeDog,
2
Jeden punkt pomocniczy - możesz wysłać dane wyjściowe do pliku, a następnie przetworzyć je później w skrypcie. W ten sposób lista plików jest dostępna do przejrzenia, jeśli potrzebujesz debugować skrypt.
user117529,

Odpowiedzi:

87

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:

  1. 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.

  2. 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.

  3. 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) ).

  4. 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.

Stéphane Chazelas
źródło
Jeśli zsh i bash są dostępne, to może być lepiej po prostu za pomocą nazw plików i Shell konstruktów zamiast próbować wykrzywiać findsię zachowywać bezpiecznie. Globowanie jest domyślnie bezpieczne, podczas gdy find jest domyślnie niebezpieczne.
Kevin,
@Kevin, patrz edycja.
Stéphane Chazelas,
182

Dlaczego zapętlenie findwyjścia jest złą praktyką?

Prosta odpowiedź brzmi:

Ponieważ nazwy plików mogą zawierać dowolny znak.

W związku z tym nie ma znaku do wydrukowania, którego można by niezawodnie użyć do rozgraniczenia nazw plików.


Znaki nowej linii są często używane (niepoprawnie) do rozgraniczenia nazw plików, ponieważ umieszczanie znaków nowej linii w nazwach plików jest niezwykłe .

Jeśli jednak zbudujesz oprogramowanie w oparciu o arbitralne założenia, w najlepszym wypadku po prostu nie będziesz w stanie poradzić sobie z nietypowymi przypadkami, aw najgorszym wypadku otworzysz się na złośliwe exploity, które dają kontrolę nad twoim systemem. To kwestia solidności i bezpieczeństwa.

Jeśli możesz pisać oprogramowanie na dwa różne sposoby, a jeden z nich poprawnie obsługuje przypadki brzegowe (nietypowe dane wejściowe), ale drugi jest łatwiejszy do odczytania, możesz argumentować, że istnieje kompromis. (Nie zrobiłbym tego. Wolę poprawny kod.)

Jeśli jednak poprawna, solidna wersja kodu jest również łatwa do odczytania, nie ma usprawiedliwienia dla pisania kodu, który zawodzi w przypadkach skrajnych. Jest tak w przypadku findi potrzeby uruchomienia polecenia dla każdego znalezionego pliku.


Mówiąc dokładniej: w systemie UNIX lub Linux nazwy plików mogą zawierać dowolny znak z wyjątkiem znaku /(który służy jako separator komponentu ścieżki) i nie mogą zawierać bajtu zerowego.

Bajt zerowy jest zatem jedynym prawidłowym sposobem ograniczania nazw plików.


Ponieważ GNU findzawiera element -print0główny, który będzie używał bajtu zerowego do rozgraniczenia nazw plików, które drukuje, GNU find można bezpiecznie używać z GNU xargsi jego -0flagą (i -rflagą) do obsługi danych wyjściowych find:

find ... -print0 | xargs -r0 ...

Nie ma jednak dobrego powodu, aby używać tego formularza, ponieważ:

  1. Dodaje zależność od findutils GNU, które nie muszą tam być, i
  2. findjest przeznaczony do uruchamiania poleceń na znalezionych plikach.

Ponadto GNU xargswymaga -0i -r, podczas gdy FreeBSD xargswymaga tylko -0(i nie ma -ropcji), a niektóre xargsnie obsługują -0wcale. Najlepiej więc trzymać się funkcji POSIX find(patrz następny rozdział) i pomijać xargs.

Jeśli chodzi o punkt 2 find- zdolność uruchamiania poleceń na znalezionych plikach - myślę, że Mike Loukides powiedział najlepiej:

findfirma ocenia wyrażenia - nie lokalizuje plików. Tak, z findpewnością lokalizuje pliki; ale to naprawdę tylko efekt uboczny.

- Elektronarzędzia Unix


Określone zastosowania POSIX find

Jaki jest właściwy sposób uruchomienia jednego lub więcej poleceń dla każdego z findwyników?

Aby uruchomić jedno polecenie dla każdego znalezionego pliku, użyj:

find dirname ... -exec somecommand {} \;

Aby uruchomić wiele poleceń w sekwencji dla każdego znalezionego pliku, gdzie drugie polecenie powinno być uruchomione tylko, jeśli pierwsze polecenie się powiedzie, użyj:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Aby uruchomić jedno polecenie na wielu plikach jednocześnie:

find dirname ... -exec somecommand {} +

find w połączeniu z sh

Jeśli potrzebujesz użyć poleceń powłoki w poleceniu, takich jak przekierowanie wyjścia lub usunięcie rozszerzenia z nazwy pliku lub czegoś podobnego, możesz skorzystać z sh -ckonstrukcji. Powinieneś wiedzieć o tym kilka rzeczy:

  • Nigdy nie osadzaj {}bezpośrednio w shkodzie. Pozwala to na wykonanie dowolnego kodu ze złośliwie spreparowanych nazw plików. Poza tym POSIX nawet nie określa, że ​​w ogóle będzie działać. (Zobacz następny punkt.)

  • Nie używaj {}wiele razy lub używaj go jako części dłuższego argumentu. To nie jest przenośne. Na przykład nie rób tego:

    find ... -exec cp {} somedir/{}.bak \;

    Cytując specyfikacje POSIX dlafind :

    Jeśli nazwa_użyteczności lub ciąg argumentu zawiera dwa znaki {{}, ale nie tylko dwa znaki {{}, to jest określone w implementacji, czy find zamienia te dwa znaki lub używa ciągu bez zmian.

    ... Jeśli występuje więcej niż jeden argument zawierający dwa znaki „{}”, zachowanie jest nieokreślone.

  • Argumenty występujące po ciągu poleceń powłoki przekazanym do -copcji są ustawiane na parametry pozycyjne powłoki, zaczynając od$0 . Nie zaczynam od $1.

    Z tego powodu dobrze jest dołączyć wartość „obojętną” $0, na przykład find-sh, która będzie używana do raportowania błędów z odradzanej powłoki. Pozwala to również na użycie konstrukcji takich jak "$@"przekazywanie wielu plików do powłoki, natomiast pominięcie wartości dla $0oznaczałoby, że pierwszy przekazany plik byłby ustawiony na, $0a zatem nie został uwzględniony "$@".


Aby uruchomić pojedyncze polecenie powłoki dla pliku, użyj:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

Jednak zwykle poprawia wydajność obsługi plików w pętli powłoki, dzięki czemu nie spawnujesz powłoki dla każdego znalezionego pliku:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Pamiętaj, że for f dojest to równoważne z for f in "$@"; dokażdym z parametrów pozycyjnych i obsługuje je kolejno - innymi słowy, używa każdego z znalezionych plików find, niezależnie od jakichkolwiek znaków specjalnych w ich nazwach).


Dalsze przykłady prawidłowego findużytkowania:

(Uwaga: przedłuż tę listę.)

Dzika karta
źródło
5
Jest jeden przypadek, w którym nie znam alternatywy dla parsowania finddanych wyjściowych - w której musisz uruchomić polecenia w bieżącej powłoce (np. Ponieważ chcesz ustawić zmienne) dla każdego pliku. W tym przypadku while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)jest to najlepszy idiom, jaki znam. Uwagi: <( )nie jest przenośny - użyj bash lub zsh. Ponadto, -u3i 3<są tam na wypadek, gdyby cokolwiek w pętli próbowało odczytać standardowe wejście.
Gordon Davisson,
1
@GordonDavisson, być może, ale co trzeba ustawić te zmienne dla ? Argumentowałbym, że cokolwiek to jest, powinno być załatwione w trakcie find ... -execrozmowy. Lub po prostu użyj globu powłoki, jeśli zajmie się twoim przypadkiem użycia.
Wildcard,
1
Często chcę wydrukować podsumowanie po przetworzeniu plików („2 przekonwertowane, 3 pominięte, następujące pliki zawierały błędy: ...”), a te liczby / listy muszą być gromadzone w zmiennych powłoki. Są też sytuacje, w których chcę utworzyć tablicę nazw plików, aby móc wykonywać bardziej złożone czynności niż iterować w kolejności (w takim przypadku jest to filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson,
3
Twoja odpowiedź jest poprawna. Jednak nie lubię dogmatu. Chociaż wiem lepiej, istnieje wiele (szczególnie interaktywnych) przypadków użycia, w których można bezpiecznie i po prostu łatwiej pisać w pętli na findwyjściu lub nawet gorzej ls. Robię to codziennie bez problemów. Wiem o opcjach -print0, --null, -z lub -0 wszelkiego rodzaju narzędzi. Ale nie marnowałbym czasu na używanie ich w interaktywnym wierszu poleceń, chyba że jest to naprawdę potrzebne. Można to również zauważyć w swojej odpowiedzi.
rudimeier
16
@ rudimeier, argument o dogmatach i najlepszych praktykach został już dokonany na śmierć . Nie zainteresowany. Jeśli używasz go interaktywnie i działa, to dobrze, dobrze dla ciebie - ale nie zamierzam promować tego. Odsetek autorów skryptów, którzy zadają sobie trud, aby dowiedzieć się, czym jest solidny kod, a następnie robią to tylko podczas pisania skryptów produkcyjnych, zamiast robić to, do czego są przyzwyczajeni interaktywnie, jest niezwykle minimalny. Postępowanie ma na celu promowanie najlepszych praktyk przez cały czas. Ludzie muszą nauczyć się, że istnieje właściwy sposób robienia rzeczy.
Wildcard
10

Ta odpowiedź dotyczy bardzo dużych zestawów wyników i dotyczy głównie wydajności, na przykład podczas pobierania listy plików w wolnej sieci. W przypadku małych ilości plików (powiedzmy kilka 100, a może nawet 1000 na dysku lokalnym) większość z nich jest dyskusyjna.

Równoległość i wykorzystanie pamięci

Oprócz innych udzielonych odpowiedzi, związanych z problemami z separacją, istnieje jeszcze inny problem

for file in `find . -type f -name ...`; do smth with ${file}; done

Część wewnątrz backticków należy najpierw w pełni ocenić, zanim zostanie podzielona w przypadku łamania linii. Oznacza to, że jeśli otrzymasz ogromną liczbę plików, może to spowodować uduszenie się, niezależnie od ograniczeń rozmiaru w różnych komponentach; możesz zabraknąć pamięci, jeśli nie ma żadnych ograniczeń; w każdym razie musisz poczekać, aż cała lista zostanie wypisana, finda następnie parsowana, forzanim uruchomisz pierwszą smth.

Preferowanym sposobem uniksowym jest praca z potokami, które z natury działają równolegle i które nie wymagają ogólnie dużych buforów. Oznacza to: wolałbyś, findaby uruchamiał się równolegle do twojego smth, i utrzymywał aktualną nazwę pliku w pamięci RAM, gdy przekazuje to smth.

Jednym z co najmniej częściowo OKish tego rozwiązania jest wyżej wspomniane find -exec smth. Eliminuje to potrzebę przechowywania wszystkich nazw plików w pamięci i działa ładnie równolegle. Niestety uruchamia również jeden smthproces na plik. Jeśli smthmoże działać tylko na jednym pliku, to właśnie tak musi być.

Jeśli to w ogóle możliwe, optymalnym rozwiązaniem byłoby find -print0 | smth, ze smthjest w stanie przetworzyć nazwy plików na swoim stdin. Następnie masz tylko jeden smthproces, bez względu na liczbę plików, i musisz buforować tylko niewielką ilość bajtów (niezależnie od tego, co dzieje się wewnętrzne buforowanie potoków) między dwoma procesami. Oczywiście jest to raczej nierealne, jeśli smthjest to standardowe polecenie Unix / POSIX, ale może być podejściem, jeśli piszesz je samodzielnie.

Jeśli nie jest to możliwe, find -print0 | xargs -0 smthjest to prawdopodobnie jedno z lepszych rozwiązań. Jak wspomniano w komentarzach @ dave_thompson_085, xargsdzieli argumenty na wiele serii smthpo osiągnięciu limitów systemowych (domyślnie w zakresie 128 KB lub dowolnego limitu narzuconego przez execsystem) i ma opcje wpływające na liczbę pliki są przekazywane do jednego wywołania smth, co pozwala znaleźć równowagę między liczbą smthprocesów a początkowym opóźnieniem.

EDYCJA: usunęła pojęcie „najlepszego” - trudno powiedzieć, czy pojawi się coś lepszego. ;)

AnoE
źródło
find ... -exec smth {} +jest rozwiązaniem.
Wildcard
find -print0 | xargs smthnie działa wcale, ale find -print0 | xargs -0 smth(uwaga -0) lub find | xargs smthjeśli nazwy plików nie mają cudzysłowów lub odwrotny ukośnik uruchamia jeden smthz tyloma nazwami plików, ile jest dostępnych i mieści się na jednej liście argumentów ; jeśli przekroczysz maxargs, działa smthtyle razy, ile potrzeba, aby obsłużyć wszystkie podane argumenty (bez limitu). Możesz ustawić mniejsze „fragmenty” (a więc nieco wcześniejszą równoległość) za pomocą -L/--max-lines -n/--max-args -s/--max-chars.
dave_thompson_085
4

Jednym z powodów jest to, że białe znaki wrzucają klucz w prace, dzięki czemu plik „foo bar” jest oceniany jako „foo” i „bar”.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Działa ok, jeśli zamiast tego użyto opcji -exec

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
Steve
źródło
Szczególnie w przypadku, findgdy istnieje opcja wykonania polecenia na każdym pliku, jest to z pewnością najlepsza opcja.
Centimane,
1
Również rozważyć -exec ... {} \;versus-exec ... {} +
thrig
1
jeśli użyjesz, for file in "$(find . -type f)" a echo "${file}"następnie będzie działać nawet z białymi spacjami, inne znaki specjalne, jak sądzę, powodują więcej problemów
mazs
9
@mazs - nie, cytowanie nie robi tego, co myślisz. W katalogu z kilkoma plikami wypróbuj, for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";donektóre powinny (według ciebie) wydrukować każdą nazwę pliku w osobnym wierszu poprzedzonym name:. Nie ma
don_crissti
2

Ponieważ wyjście dowolnego polecenia jest pojedynczym ciągiem, ale twoja pętla potrzebuje tablicy ciągów do zapętlenia. Powodem, dla którego „działa”, jest to, że pociski zdradzająco dzielą cię na biały znak.

Po drugie, chyba że potrzebujesz konkretnej funkcji find, pamiętaj, że twoja powłoka najprawdopodobniej już sama może rozwinąć rekurencyjny wzorzec globu i, co najważniejsze, rozszerzy się do odpowiedniej tablicy.

Przykład bash:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

To samo u ryb:

for i in **
    echo «$i»
end

Jeśli potrzebujesz funkcji find, upewnij się, że dzielisz tylko na NUL (np. find -print0 | xargs -r0Idiom).

Ryby mogą iterować dane rozdzielane wartościami NUL. Więc to nie jest wcale takie złe:

find -print0 | while read -z i
    echo «$i»
end

Jako ostatnia mała gotcha, w wielu powłokach (oczywiście nie Fish), zapętlenie nad wyjściem polecenia spowoduje, że ciało pętli stanie się podpowłoką (co oznacza, że ​​nie można ustawić zmiennej w żaden sposób widoczny po zakończeniu pętli), co jest nigdy tego, czego chcesz.

użytkownik2394284
źródło
@don_crissti Dokładnie. To na ogół nie działa. Próbowałem być sarkastyczny, mówiąc, że „działa” (z cytatami).
user2394284,
Zwróć uwagę, że globowanie rekurencyjne powstało zshna początku lat 90. (choć będziesz tego potrzebować **/*). fishpodobnie jak wcześniejsze implementacje równoważnej funkcji bash podążają za dowiązaniami symbolicznymi podczas schodzenia z drzewa katalogów. Zobacz wynik ls *, ls ** i ls ***, aby zobaczyć różnice między implementacjami.
Stéphane Chazelas
1

Pętla wyników wyszukiwania nie jest złą praktyką - złą praktyką (w tej i wszystkich sytuacjach) jest zakładanie, że dane wejściowe mają określony format, a nie wiedza (testowanie i potwierdzanie), że jest to określony format.

tldr / cbf: find | parallel stuff

Jan Kyu Peblik
źródło