dla vs znaleźć w Bash

28

Pętlowanie plików ma dwa sposoby:

  1. użyj forpętli:

    for f in *; do
        echo "$f"
    done
    
  2. użyj find:

    find * -prune | while read f; do 
        echo "$f"
    done
    

Zakładając, że te dwie pętle znajdą tę samą listę plików, jakie są różnice między tymi dwiema opcjami w zakresie wydajności i obsługi?

rubo77
źródło
1
Czemu? findnie otwiera znalezionych plików. Jedyne, co widzę, gryząc cię tutaj w odniesieniu do dużej liczby plików, to ARG_MAX .
kojiro
1
Zobacz odpowiedzi i komentarze, które powiedzą, że read fzmieniają nazwy plików podczas ich odczytywania (np. Nazwy z wiodącymi odstępami). find * -pruneWydaje się również, że jest to bardzo skomplikowany sposób na powiedzenie po prostu ls -1tak?
Ian D. Allen
4
Nie zakładaj, że dwie pętle znajdą ten sam zestaw plików; w większości przypadków nie będą. Również, że powinno być find ., nie find *.
Alexis
1
@terdon Tak, parsowanie ls -lto zły pomysł. Ale parsowanie ls -1(to 1nie jest an l) nie jest gorsze niż parsowanie find * -prune. Oba zawodzą w plikach z nowymi liniami w nazwach.
Ian D. Allen
5
Podejrzewam, że każdy z nas spędził więcej czasu na czytaniu tego pytania i odpowiedzi niż całkowita różnica w wydajności w ciągu życia danego skryptu.
mpez0,

Odpowiedzi:

9

1.

Pierwszy:

for f in *; do
  echo "$f"
done

nie plików o nazwie -n, -ei warianty jak -neneiz niektórych wdrożeniach bash, ale w nazwach zawierających backslashy.

Drugi:

find * -prune | while read f; do 
  echo "$f"
done

nie dla jeszcze większej liczby przypadków (plików o nazwie !, -H, -name, (, nazw plików, które rozpoczynają lub kończą się puste lub zawierają znaki nowej linii ...)

To powłoka, która się rozszerza *, findnic nie robi, tylko drukuje pliki, które otrzymuje jako argumenty. Równie dobrze możesz użyć printf '%s\n'zamiast tego, który printfjest wbudowany, aby uniknąć zbyt wielu potencjalnych błędów args .

2)

Rozszerzenie *jest posortowane, możesz je przyspieszyć, jeśli nie potrzebujesz sortowania. W zsh:

for f (*(oN)) printf '%s\n' $f

lub po prostu:

printf '%s\n' *(oN)

basho ile mi wiadomo, nie ma odpowiednika, więc musisz się do niego odwołać find.

3)

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(powyżej przy użyciu -print0niestandardowego rozszerzenia GNU / BSD ).

To wciąż wymaga odrodzenia polecenia find i użycia wolnej while readpętli, więc prawdopodobnie będzie wolniejsza niż użycie forpętli, chyba że lista plików jest ogromna.

4

Ponadto, w przeciwieństwie do rozszerzenia symboli wieloznacznych powłoki, findwykona lstatwywołanie systemowe dla każdego pliku, więc jest mało prawdopodobne, aby brak sortowania to zrekompensował.

W przypadku GNU / BSD findmożna tego uniknąć, stosując ich -maxdepthrozszerzenie, które uruchomi optymalizację oszczędzając lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Ponieważ findzaczyna wypisywać nazwy plików, gdy tylko je znajdzie (z wyjątkiem buforowania wyjścia stdio), dlatego może być szybsze, jeśli to, co robisz w pętli, jest czasochłonne, a lista nazw plików to więcej niż bufor stdio (4 / 8 kB). W takim przypadku przetwarzanie w pętli rozpocznie się przed findzakończeniem wyszukiwania wszystkich plików. W systemach GNU i FreeBSD możesz użyć tego, stdbufaby spowodować to wcześniej (wyłączenie buforowania stdio).

5

POSIX / standard / przenośny sposób uruchamiania poleceń dla każdego pliku za findpomocą -execpredykatu:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

W tym przypadku jest to echojednak mniej wydajne niż wykonywanie pętli w powłoce, ponieważ powłoka będzie miała wbudowaną wersję, echopodczas gdy findbędzie wymagać odrodzenia nowego procesu i wykonania /bin/echow nim dla każdego pliku.

Jeśli chcesz uruchomić kilka poleceń, możesz:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Ale uważaj, że cmd2jest wykonywany tylko wtedy, gdy cmd1się powiedzie.

6.

Kanonicznym sposobem uruchamiania złożonych poleceń dla każdego pliku jest wywołanie powłoki za pomocą -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Tym razem wracamy do sprawności, echoponieważ używamy shwbudowanego, a -exec +wersja odradza się shjak najmniej.

7

W moich testach katalogu z 200 000 plików o krótkich nazwach na ext4 ten zsh(paragraf 2) jest zdecydowanie najszybszy, a następnie pierwsza prosta for i in *pętla (choć jak zwykle bashjest znacznie wolniejsza niż inne powłoki).

Stéphane Chazelas
źródło
co robi !polecenie find?
rubo77
@ rubo77, !służy do negacji. ! -name . -prune more...zrobi -prune(i more...ponieważ -prunezawsze zwraca true) dla każdego pliku, ale .. Tak więc zrobi to more...na wszystkich plikach w ., ale wykluczy .i nie zejdzie do podkatalogów .. Jest to standardowy odpowiednik GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

Próbowałem tego w katalogu z 2259 wpisami i użyłem timepolecenia.

Dane wyjściowe time for f in *; do echo "$f"; done(bez plików!) To:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

Dane wyjściowe time find * -prune | while read f; do echo "$f"; done(bez plików!) To:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Uruchomiłem każdą komendę kilka razy, aby wyeliminować błędy w pamięci podręcznej. Sugeruje to, że trzymanie go w bash(dla i w ...) jest szybsze niż używanie findi przesyłanie danych wyjściowych (do bash)

Dla kompletności zrzuciłem fajkę find, ponieważ w twoim przykładzie jest całkowicie zbędna. Wynikiem just find * -prunejest:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Ponadto time echo *(wyjście nie jest oddzielone znakiem nowej linii, niestety):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

W tym momencie podejrzewam, że powodem echo *jest szybsze to, że nie wyświetla tak wielu nowych linii, więc wynik nie przewija się tak bardzo. Przetestujmy ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

daje:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

podczas gdy time find * -prune > /dev/nulldaje:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

i time for f in *; do echo "$f"; done > /dev/nulldaje:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

i wreszcie: time echo * > /dev/nulldaje:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Niektóre warianty można przypisać przypadkowym czynnikom, ale wydaje się jasne:

  • wyjście jest wolne
  • orurowanie kosztuje trochę
  • for f in *; do ...jest wolniejszy niż find * -prunesam, ale w przypadku powyższych konstrukcji z rurami jest szybszy.

Nawiasem mówiąc, oba podejścia wydają się obsługiwać nazwy ze spacjami w porządku.

EDYTOWAĆ:

Czasy dla find . -maxdepth 1 > /dev/nullvs. find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Tak więc dodatkowy wniosek:

  • find * -prunejest wolniejszy niż find . -maxdepth 1- w pierwszym przypadku powłoka przetwarza glob, a następnie buduje (dużą) linię poleceń find. NB: find . -prunewłaśnie wraca ..

Więcej testów time find . -maxdepth 1 -exec echo {} \; >/dev/null:

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Wniosek:

  • jak dotąd najwolniejszy sposób. Jak wskazano w komentarzach do odpowiedzi, w której sugerowano takie podejście, każdy argument tworzy spaw.
Phil
źródło
Która rura jest zbędna? czy możesz pokazać linię, której użyłeś bez rury?
rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; donema redundantny potok - wszystko, co robi potok, wyprowadza dokładnie to, co findwyprowadza samodzielnie. Bez rury byłoby to po prostu find * -prune . Rura jest redundantna tylko dlatego, że rzecz po drugiej stronie rury po prostu kopiuje stdin na stdout (w przeważającej części). To kosztowny brak op. Jeśli chcesz robić rzeczy z wyjściem find, inne niż po prostu wypluć je ponownie, jest inaczej.
Phil
Być może głównym pochłaniającym czas jest *. Jak BitsOfNix stwierdził: ja nadal silnie sugerują, aby nie używać *i .dla findzamiast.
rubo77
@ rubo77 wydaje się w ten sposób. Chyba przeoczyłem to. Dodałem ustalenia dla mojego systemu. Zakładam, że find . -prunejest szybszy, ponieważ findbędzie czytać pozycję katalogu dosłownie, podczas gdy powłoka będzie działać podobnie, potencjalnie dopasowując się do globu (może się zoptymalizować *), a następnie budując dużą linię poleceń find.
Phil
1
find . -prunedrukuje tylko .w moim systemie. To prawie nie działa. To wcale nie to samo, co find * -prunepokazuje wszystkie nazwy w bieżącym katalogu. Gołe read fbędzie zmieniać nazwy plików z wiodącymi spacjami.
Ian D. Allen
10

Zdecydowanie wybrałbym find, chociaż zmieniłbym twoje znalezisko na następujące:

find . -maxdepth 1 -exec echo {} \;

findOczywiście pod względem wydajności jest znacznie szybszy, w zależności od potrzeb. To, co aktualnie masz, forwyświetli tylko pliki / katalogi w bieżącym katalogu, ale nie zawartość katalogów. Jeśli użyjesz find, pokaże także zawartość podkatalogów.

Mówię, że find jest lepszy, ponieważ w twoim forprzypadku *trzeba go najpierw rozwinąć i obawiam się, że jeśli masz katalog z dużą ilością plików, może to oznaczać zbyt długą listę argumentów błędów . To samo dotyczyfind *

Na przykład w jednym z systemów, z których obecnie korzystam, jest kilka katalogów z ponad 2 milionami plików (<100 KB każdy):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
źródło
Dodałem, -pruneaby dwa przykłady były bardziej podobne. a ja wolę fajkę z while, więc łatwiej jest zastosować więcej poleceń w pętli
rubo77
zmiana twardego limitu nie jest właściwym obejściem mojego POV. Zwłaszcza w przypadku ponad 2 milionów plików. Bez dygresji z pytania, dla prostych przypadków, ponieważ katalog o jednym poziomie jest szybszy, ale jeśli zmienisz strukturę pliku / katalogu, migracja będzie trudniejsza. Korzystając z funkcji wyszukiwania i ogromnej liczby opcji, możesz być lepiej przygotowany. Nadal zdecydowanie sugeruję, aby nie używać * i. zamiast znaleźć. Byłby bardziej przenośny niż *, gdzie możesz nie być w stanie kontrolować ograniczenia ...
BitsOfNix,
4
Spowoduje to odrodzenie jednego procesu echa na plik (w powłoce dla pętli jest to wbudowane echo, które będzie używane bez tworzenia dodatkowego procesu), i przejdzie do katalogów, więc będzie o wiele wolniej . Pamiętaj też, że będzie zawierać pliki kropkowe.
Stéphane Chazelas
Masz rację, dodałem maxdepth 1, aby przylegał tylko do bieżącego poziomu.
BitsOfNix,
7
find * -prune | while read f; do 
    echo "$f"
done

to bezużyteczne użycie find- To, co mówisz, jest efektywne "dla każdego pliku w katalogu ( *), nie znajdź żadnych plików. Ponadto nie jest to bezpieczne z kilku powodów:

  • Odwrotne ukośniki na ścieżkach są traktowane specjalnie bez -ropcji read. To nie jest problem z forpętlą.
  • Nowe linie na ścieżkach złamałyby wszelkie nietrywialne funkcje wewnątrz pętli. To nie jest problem z forpętlą.

Obsługa dowolnej nazwy pliku findjest trudna , dlatego powinieneś używać foropcji pętli, gdy tylko jest to możliwe z tego samego powodu. Ponadto uruchomienie zewnętrznego programu, takiego jak, findbędzie ogólnie wolniejsze niż uruchomienie wewnętrznego polecenia pętli, takiego jak for.

l0b0
źródło
@ I0b0 Co z find -path './*' -prune or find -path './[^.]*' -prune (aby uniknąć ukrytych plików i katalogów) jako lepsza konstrukcja - w pełnej formie: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Ani find-print0ani xargs” nie -0są kompatybilne z POSIX i nie można wstawiać dowolnych poleceń sh -c ' ... '(pojedynczych cudzysłowów nie można uciec w pojedynczych cudzysłowach), więc nie jest to takie proste.
l0b0
4

Ale jesteśmy frajerami w kwestiach dotyczących wydajności! To żądanie eksperymentu przyjmuje co najmniej dwa założenia, które sprawiają, że nie jest on zbyt ważny.

A. Załóżmy, że znajdują te same pliki…

Dobrze, że będzie znaleźć te same pliki na początku, bo oboje Iteracja nad samym glob, a mianowicie *. Ale find * -prune | while read fma kilka wad, które sprawiają, że jest całkiem możliwe, że nie znajdzie wszystkich oczekiwanych plików:

  1. POSIX find nie gwarantuje zaakceptowania więcej niż jednego argumentu ścieżki. Większość findimplementacji to robi, ale nie powinieneś na tym polegać.
  2. find *może pęknąć po trafieniu ARG_MAX. for f in *nie będzie, ponieważ ARG_MAXdotyczy execniewbudowanych.
  3. while read fmoże zerwać z nazwami plików rozpoczynającymi się i kończącymi na białych znakach, które zostaną usunięte. Można temu zaradzić za pomocą while readparametru domyślnego REPLY, ale to nadal nie pomoże, jeśli chodzi o nazwy plików z nowymi liniami.

B. echo. Nikt tego nie zrobi, aby powtórzyć nazwę pliku. Jeśli chcesz, po prostu wykonaj jedną z następujących czynności:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

Potok do whilepętli tutaj tworzy niejawną podpowłokę, która zamyka się, gdy pętla się kończy, co może być nieintuicyjne dla niektórych.

Aby odpowiedzieć na pytanie, oto wyniki w moim katalogu, który zawiera 184 pliki i katalogi.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s
kojiro
źródło
Nie zgadzam się z tym stwierdzeniem, że pętla while odradza podpowłokę - w najgorszym przypadku nowy wątek: następujące próbuje pokazać przed i po, przepraszam za złe formatowanie$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil
Technicznie popełniłem błąd: potok wywołuje ukrytą podpowłokę, a nie pętlę while. Będę edytować.
kojiro
2

find *nie będzie działać poprawnie, jeśli *produkuje tokeny, które wyglądają jak predykaty, a nie ścieżki.

Nie można użyć zwykłego --argumentu, aby to naprawić, ponieważ --wskazuje koniec opcji, a opcje find znajdują się przed ścieżkami.

Aby rozwiązać ten problem, możesz użyć find ./*zamiast tego. Ale wtedy nie produkuje dokładnie takich samych łańcuchów jak for x in *.

Pamiętaj, że find ./* -prune | while read f ..tak naprawdę nie korzysta z funkcji skanowania find. Jest to składnia globowania, ./*która faktycznie przegląda katalog i generuje nazwy. Następnie findprogram będzie musiał wykonać co najmniej statsprawdzenie każdej z tych nazw. Masz narzut związany z uruchomieniem programu i dostępem do tych plików, a następnie wykonywaniem operacji we / wy w celu odczytania jego danych wyjściowych.

Trudno sobie wyobrazić, jak mogłoby być mniej wydajne niż for x in ./* ....

Kaz
źródło
1

Na początek forjest słowem kluczowym powłoki, wbudowanym w Bash, podczas gdy findjest osobnym plikiem wykonywalnym.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

forPętla znajdzie tylko pliki z charakterem globstar gdy rozszerza się, to nie będzie rekursja do wszelkich katalogów znajdzie.

Find z drugiej strony otrzyma również listę rozwiniętą przez globstar, ale rekurencyjnie znajdzie wszystkie pliki i katalogi poniżej tej rozszerzonej listy i poprowadzi każdy z nich do whilepętli.

Oba te podejścia można uznać za niebezpieczne w tym sensie, że nie obsługują ścieżek ani nazw plików zawierających spacje.

To wszystko, co mogę wymyślić, aby skomentować te dwa podejścia.

slm
źródło
Dodałem -prune do polecenia find, więc są one bardziej do siebie podobne.
rubo77
0

Jeśli wszystkie pliki zwrócone przez find można przetworzyć za pomocą jednego polecenia (oczywiście nie dotyczy powyższego przykładu echa), możesz użyć xargs:

find * |xargs some-command
Obrabować
źródło
0

Od lat używam tego: -

find . -name 'filename'|xargs grep 'pattern'|more

aby wyszukać określone pliki (np. * .txt), które zawierają wzorzec, którego grep może szukać, i potokuj go bardziej, aby nie przewinął się z ekranu. Czasami używam potoku >> do zapisywania wyników w innym pliku, który mogę obejrzeć później.

Oto próbka wyniku: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Allen
źródło