Pętlowanie plików ma dwa sposoby:
użyj
for
pętli:for f in *; do echo "$f" done
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?
bash
shell-script
performance
rubo77
źródło
źródło
find
nie otwiera znalezionych plików. Jedyne, co widzę, gryząc cię tutaj w odniesieniu do dużej liczby plików, to ARG_MAX .read f
zmieniają nazwy plików podczas ich odczytywania (np. Nazwy z wiodącymi odstępami).find * -prune
Wydaje się również, że jest to bardzo skomplikowany sposób na powiedzenie po prostuls -1
tak?find .
, niefind *
.ls -l
to zły pomysł. Ale parsowaniels -1
(to1
nie jest anl
) nie jest gorsze niż parsowaniefind * -prune
. Oba zawodzą w plikach z nowymi liniami w nazwach.Odpowiedzi:
1.
Pierwszy:
nie plików o nazwie
-n
,-e
i warianty jak-nene
iz niektórych wdrożeniach bash, ale w nazwach zawierających backslashy.Drugi:
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
*
,find
nic nie robi, tylko drukuje pliki, które otrzymuje jako argumenty. Równie dobrze możesz użyćprintf '%s\n'
zamiast tego, któryprintf
jest wbudowany, aby uniknąć zbyt wielu potencjalnych błędów args .2)
Rozszerzenie
*
jest posortowane, możesz je przyspieszyć, jeśli nie potrzebujesz sortowania. Wzsh
:lub po prostu:
bash
o ile mi wiadomo, nie ma odpowiednika, więc musisz się do niego odwołaćfind
.3)
(powyżej przy użyciu
-print0
niestandardowego rozszerzenia GNU / BSD ).To wciąż wymaga odrodzenia polecenia find i użycia wolnej
while read
pętli, więc prawdopodobnie będzie wolniejsza niż użyciefor
pętli, chyba że lista plików jest ogromna.4
Ponadto, w przeciwieństwie do rozszerzenia symboli wieloznacznych powłoki,
find
wykonalstat
wywołanie systemowe dla każdego pliku, więc jest mało prawdopodobne, aby brak sortowania to zrekompensował.W przypadku GNU / BSD
find
można tego uniknąć, stosując ich-maxdepth
rozszerzenie, które uruchomi optymalizację oszczędzająclstat
:Ponieważ
find
zaczyna 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ę przedfind
zakończeniem wyszukiwania wszystkich plików. W systemach GNU i FreeBSD możesz użyć tego,stdbuf
aby spowodować to wcześniej (wyłączenie buforowania stdio).5
POSIX / standard / przenośny sposób uruchamiania poleceń dla każdego pliku za
find
pomocą-exec
predykatu:W tym przypadku jest to
echo
jednak mniej wydajne niż wykonywanie pętli w powłoce, ponieważ powłoka będzie miała wbudowaną wersję,echo
podczas gdyfind
będzie wymagać odrodzenia nowego procesu i wykonania/bin/echo
w nim dla każdego pliku.Jeśli chcesz uruchomić kilka poleceń, możesz:
Ale uważaj, że
cmd2
jest wykonywany tylko wtedy, gdycmd1
się powiedzie.6.
Kanonicznym sposobem uruchamiania złożonych poleceń dla każdego pliku jest wywołanie powłoki za pomocą
-exec ... {} +
:Tym razem wracamy do sprawności,
echo
ponieważ używamysh
wbudowanego, a-exec +
wersja odradza sięsh
jak 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 prostafor i in *
pętla (choć jak zwyklebash
jest znacznie wolniejsza niż inne powłoki).źródło
!
polecenie find?!
służy do negacji.! -name . -prune more...
zrobi-prune
(imore...
ponieważ-prune
zawsze zwraca true) dla każdego pliku, ale.
. Tak więc zrobi tomore...
na wszystkich plikach w.
, ale wykluczy.
i nie zejdzie do podkatalogów.
. Jest to standardowy odpowiednik GNU-mindepth 1 -maxdepth 1
.Próbowałem tego w katalogu z 2259 wpisami i użyłem
time
polecenia.Dane wyjściowe
time for f in *; do echo "$f"; done
(bez plików!) To:Dane wyjściowe
time find * -prune | while read f; do echo "$f"; done
(bez plików!) To: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żywaniefind
i przesyłanie danych wyjściowych (dobash
)Dla kompletności zrzuciłem fajkę
find
, ponieważ w twoim przykładzie jest całkowicie zbędna. Wynikiem justfind * -prune
jest:Ponadto
time echo *
(wyjście nie jest oddzielone znakiem nowej linii, niestety):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 ...daje:
podczas gdy
time find * -prune > /dev/null
daje:i
time for f in *; do echo "$f"; done > /dev/null
daje:i wreszcie:
time echo * > /dev/null
daje:Niektóre warianty można przypisać przypadkowym czynnikom, ale wydaje się jasne:
for f in *; do ...
jest wolniejszy niżfind * -prune
sam, 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/null
vs.find * -prune > /dev/null
:time find . -maxdepth 1 > /dev/null
:find * -prune > /dev/null
:Tak więc dodatkowy wniosek:
find * -prune
jest wolniejszy niżfind . -maxdepth 1
- w pierwszym przypadku powłoka przetwarza glob, a następnie buduje (dużą) linię poleceńfind
. NB:find . -prune
właśnie wraca.
.Więcej testów
time find . -maxdepth 1 -exec echo {} \; >/dev/null
:Wniosek:
źródło
find * -prune | while read f; do echo "$f"; done
ma redundantny potok - wszystko, co robi potok, wyprowadza dokładnie to, cofind
wyprowadza samodzielnie. Bez rury byłoby to po prostufind * -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.*
. Jak BitsOfNix stwierdził: ja nadal silnie sugerują, aby nie używać*
i.
dlafind
zamiast.find . -prune
jest szybszy, ponieważfind
bę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
.find . -prune
drukuje tylko.
w moim systemie. To prawie nie działa. To wcale nie to samo, cofind * -prune
pokazuje wszystkie nazwy w bieżącym katalogu. Gołeread f
będzie zmieniać nazwy plików z wiodącymi spacjami.Zdecydowanie wybrałbym find, chociaż zmieniłbym twoje znalezisko na następujące:
find
Oczywiście pod względem wydajności jest znacznie szybszy, w zależności od potrzeb. To, co aktualnie masz,for
wyś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
for
przypadku*
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):
źródło
-prune
aby dwa przykłady były bardziej podobne. a ja wolę fajkę z while, więc łatwiej jest zastosować więcej poleceń w pętlito 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:-r
opcjiread
. To nie jest problem zfor
pętlą.for
pętlą.Obsługa dowolnej nazwy pliku
find
jest trudna , dlatego powinieneś używaćfor
opcji pętli, gdy tylko jest to możliwe z tego samego powodu. Ponadto uruchomienie zewnętrznego programu, takiego jak,find
będzie ogólnie wolniejsze niż uruchomienie wewnętrznego polecenia pętli, takiego jakfor
.źródło
find
„-print0
anixargs
” nie-0
są 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.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
*
. Alefind * -prune | while read f
ma kilka wad, które sprawiają, że jest całkiem możliwe, że nie znajdzie wszystkich oczekiwanych plików:find
implementacji to robi, ale nie powinieneś na tym polegać.find *
może pęknąć po trafieniuARG_MAX
.for f in *
nie będzie, ponieważARG_MAX
dotyczyexec
niewbudowanych.while read f
moż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 read
parametru domyślnegoREPLY
, 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:Potok do
while
pę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.
źródło
$ 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
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 jakfor x in *
.Pamiętaj, że
find ./* -prune | while read f ..
tak naprawdę nie korzysta z funkcji skanowaniafind
. Jest to składnia globowania,./*
która faktycznie przegląda katalog i generuje nazwy. Następniefind
program będzie musiał wykonać co najmniejstat
sprawdzenie 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 ./* ...
.źródło
Na początek
for
jest słowem kluczowym powłoki, wbudowanym w Bash, podczas gdyfind
jest osobnym plikiem wykonywalnym.for
Pę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
while
pę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.
źródło
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:
źródło
Od lat używam tego: -
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: -
źródło