Jak przeglądać nazwy plików zwracane przez find?

223
x=$(find . -name "*.txt")
echo $x

jeśli uruchomię powyższy fragment kodu w powłoce Bash, otrzymam ciąg zawierający kilka nazw plików oddzielonych spacją, a nie listę.

Oczywiście mogę je dalej oddzielić pustymi, aby uzyskać listę, ale jestem pewien, że jest lepszy sposób, aby to zrobić.

Więc jaki jest najlepszy sposób na przeglądanie wyników findpolecenia?

Haiyuan Zhang
źródło
3
Najlepszy sposób na zapętlanie nazw plików zależy w dużej mierze od tego, co naprawdę chcesz z nimi zrobić, ale jeśli nie możesz zagwarantować, że żadne pliki nie mają białych znaków w nazwie, nie jest to świetny sposób na zrobienie tego. Więc co chcesz zrobić, zapętlając pliki?
Kevin
1
Jeśli chodzi o nagrodę : główną ideą tutaj jest uzyskanie kanonicznej odpowiedzi obejmującej wszystkie możliwe przypadki (nazwy plików z nowymi wierszami, problematyczne znaki ...). Chodzi o to, aby użyć tych nazw plików do wykonania pewnych czynności (wywołanie innego polecenia, wykonanie zmiany nazwy ...). Dzięki!
fedorqui „SO przestań krzywdzić”
Nie zapominaj, że nazwa pliku lub folderu może zawierać „.txt”, po którym następuje spacja i inny ciąg, na przykład „
coś.txt
Użyj tablicy, a nie var. x=( $(find . -name "*.txt") ); echo "${x[@]}"Następnie możesz przejść przez pętlęfor item in "${x[@]}"; { echo "$item"; }
Ivan

Odpowiedzi:

393

TL; DR: Jeśli jesteś tutaj, aby uzyskać najbardziej poprawną odpowiedź, prawdopodobnie chcesz moich osobistych preferencji find . -name '*.txt' -exec process {} \;(patrz na dole tego postu). Jeśli masz czas, przeczytaj resztę, aby zobaczyć kilka różnych sposobów i problemów z większością z nich.


Pełna odpowiedź:

Najlepszy sposób zależy od tego, co chcesz zrobić, ale oto kilka opcji. Tak długo, jak żaden plik lub folder w poddrzewie nie ma spacji w nazwie, możesz po prostu zapętlić pliki:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Lepiej marginalnie, wytnij zmienną tymczasową x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

O wiele lepiej jest globować, kiedy możesz. Bezpieczny spacja dla plików w bieżącym katalogu:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Włączając tę globstaropcję, możesz globować wszystkie pasujące pliki w tym katalogu i we wszystkich podkatalogach:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

W niektórych przypadkach, np. Jeśli nazwy plików są już w pliku, może być konieczne użycie read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readmoże być bezpiecznie używany w połączeniu z findpoprzez odpowiednie ustawienie ogranicznika:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

W przypadku bardziej skomplikowanych wyszukiwań prawdopodobnie będziesz chciał użyć findtej -execopcji lub z -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findmoże także cd do katalogu każdego pliku przed uruchomieniem polecenia za pomocą -execdirzamiast -exec, i może być interaktywny (monit przed uruchomieniem polecenia dla każdego pliku) za pomocą -okzamiast -exec(lub -okdirzamiast -execdir).

*: Technicznie zarówno findi xargs(domyślnie) uruchomią polecenie z tyloma argumentami, ile mogą zmieścić się w wierszu poleceń, tyle razy, ile potrzeba, aby przejść przez wszystkie pliki. W praktyce, chyba że masz bardzo dużą liczbę plików, nie będzie to miało znaczenia, a jeśli przekroczysz długość, ale potrzebujesz ich wszystkich w tym samym wierszu poleceń, SOL znajdzie inną drogę.

Kevin
źródło
4
Warto zauważyć, że w przypadku z done < filenamei następnym z rurą stdin nie może być stosowany dłużej (→ nie więcej interaktywnej rzeczy wewnątrz pętli), ale w przypadkach, gdy jest to potrzebne można stosować 3<zamiast <dodać <&3lub -u3do readczęść, w zasadzie za pomocą oddzielnego deskryptor pliku. Ponadto uważam, że read -d ''jest taki sam, read -d $'\0'ale nie mogę teraz znaleźć żadnej oficjalnej dokumentacji na ten temat.
phk
1
dla i w * .txt; nie działa, jeśli nie ma pasujących plików. Potrzebny jest jeden test xtra np. [[-E $ i]]
Michael Brux
2
Zagubiłem się w tej części: -exec process {} \;i przypuszczam, że to zupełnie inne pytanie - co to znaczy i jak to manipulować? Gdzie jest dobra Q / A lub doc. na tym?
Alex Hall,
1
@AlexHall zawsze możesz spojrzeć na strony man ( man find). W takim przypadku -execnakazuje findwykonanie następującego polecenia, zakończonego znakiem ;(lub +), w którym {}zostanie zastąpiony nazwą pliku, który przetwarza (lub, jeśli +jest używany, wszystkich plików, które spełniły ten warunek).
Kevin,
3
@phk -d ''jest lepszy niż -d $'\0'. To ostatnie jest nie tylko dłuższe, ale sugeruje również, że możesz przekazać argumenty zawierające bajty puste, ale nie możesz. Pierwszy bajt zerowy oznacza koniec łańcucha. W bash $'a\0bc'jest taki sam jak ai $'\0'jest taki sam jak $'\0abc'lub tylko pusty ciąg ''. help readstwierdza, że ​​„ Pierwszy znak delimu służy do zakończenia wprowadzania ”, więc użycie ''jako separatora jest trochę włamaniem. Pierwszym znakiem w pustym łańcuchu jest bajt zerowy, który zawsze oznacza koniec łańcucha (nawet jeśli nie zapisujesz go wprost).
Socowi,
114

Cokolwiek robisz, nie używaj forpętli :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Trzy powody:

  • Aby pętla for mogła się nawet uruchomić, findnależy uruchomić do końca.
  • Jeśli nazwa pliku zawiera jakieś białe znaki (w tym spację, tabulator lub znak nowej linii), będzie traktowana jak dwie osobne nazwy.
  • Chociaż teraz jest to mało prawdopodobne, możesz przekroczyć bufor linii poleceń. Wyobraź sobie, że bufor linii poleceń ma 32 KB, a twoja forpętla zwraca 40 KB tekstu. Ostatnie 8 KB zostanie zrzucone z forpętli i nigdy się tego nie dowiesz.

Zawsze używaj while readkonstrukcji:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Pętla zostanie wykonana podczas wykonywania findpolecenia. Ponadto to polecenie będzie działać, nawet jeśli nazwa pliku zostanie zwrócona z białymi spacjami. I nie przepełnisz bufora wiersza poleceń.

-print0Użyje NULL jako separator pliku zamiast nowej linii i -d $'\0'użyje NULL jako separator podczas czytania.

David W.
źródło
3
Nie będzie działać z nowymi liniami w nazwach plików. -execZamiast tego użyj find's .
użytkownik nieznany
2
@userunknown - Masz rację. -execjest najbezpieczniejszy, ponieważ w ogóle nie używa powłoki. Jednak NL w nazwach plików jest dość rzadkie. Spacje w nazwach plików są dość powszechne. Najważniejsze jest, aby nie używać forpętli zalecanej przez wiele plakatów.
David W.
1
@uunkunknown - tutaj. Naprawiłem to, więc teraz zajmie się plikami z nowymi liniami, tabulatorami i innymi białymi spacjami. Chodzi o to, aby powiedzieć OP, aby nie korzystał z for file $(find)powodu problemów z tym związanych.
David W.
4
Jeśli możesz użyć -exec, jest lepiej, ale są chwile, kiedy naprawdę potrzebujesz nazwy zwróconej powłoce. Na przykład, jeśli chcesz usunąć rozszerzenia plików.
Ben Reser
5
Powinieneś użyć tej -ropcji, aby read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Uwaga: tej metody i (drugiej) metody przedstawionej przez bmargulies można bezpiecznie używać z białymi spacjami w nazwach plików / folderów.

Aby uwzględnić także - nieco egzotyczny - przypadek znaków nowej linii w nazwach plików / folderów, będziesz musiał skorzystać z -execpredykatu findtakiego:

find . -name '*.txt' -exec echo "{}" \;

{}Jest zastępczy dla elementu znajdując i \;służy do zakończenia -execorzecznik.

A dla kompletności dodam jeszcze jeden wariant - musisz pokochać * nix sposoby ich wszechstronności:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

To by oddzieliło drukowane elementy \0znakiem, który nie jest dozwolony w żadnym systemie plików w nazwach plików lub folderów, o ile wiem, i dlatego powinien obejmować wszystkie podstawy. xargszbiera je kolejno jeden po drugim ...

0xC0000022L
źródło
3
Nie działa, jeśli nowa nazwa pliku.
użytkownik nieznany
2
@ użytkownik nieznany: masz rację, to przypadek, którego w ogóle nie rozpatrzyłem i myślę, że jest to bardzo egzotyczne. Ale odpowiednio dostosowałem swoją odpowiedź.
0xC0000022L,
5
Prawdopodobnie warto to zaznaczyć find -print0i xargs -0są to zarówno argumenty rozszerzenia GNU, jak i nieprzenośne (POSIX) argumenty. Niezwykle przydatne w systemach, które je mają!
Toby Speight
1
To również kończy się niepowodzeniem w przypadku nazw plików zawierających odwrotne ukośniki (które read -rmogłyby naprawić) lub nazw plików kończących się białymi spacjami (które IFS= readmogłyby naprawić). Stąd BashFAQ # 1 sugerujewhile IFS= read -r filename; do ...
Charles Duffy
1
Innym problemem jest to, że wygląda na to, że ciało pętli działa w tej samej powłoce, ale tak nie jest, więc na przykład exitnie będzie działać zgodnie z oczekiwaniami, a zmienne ustawione w ciele pętli nie będą dostępne po pętli.
EM0
17

Nazwy plików mogą zawierać spacje, a nawet znaki kontrolne. Spacje są (domyślnymi) ogranicznikami rozszerzania powłoki w bash i dlatego x=$(find . -name "*.txt")nie są w ogóle zalecane. Jeśli find otrzyma nazwę pliku ze spacjami, np. "the file.txt"Otrzymasz 2 oddzielne ciągi do przetworzenia, jeśli przetworzysz xw pętli. Możesz to poprawić, zmieniając separator ( IFSzmienna bash ) np. Na \r\n, ale nazwy plików mogą zawierać znaki kontrolne - więc nie jest to (całkowicie) bezpieczna metoda.

Z mojego punktu widzenia istnieją 2 zalecane (i bezpieczne) wzorce przetwarzania plików:

1. Użyj do rozwijania pętli i nazw plików:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Użyj funkcji znajdź odczyt podczas odczytu i podstawienia procesu

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Uwagi

na wzorze 1:

  1. bash zwraca wzorzec wyszukiwania („* .txt”), jeśli nie znaleziono pasującego pliku - więc potrzebny jest dodatkowy wiersz „kontynuuj, jeśli plik nie istnieje”. patrz Podręcznik Bash, Rozszerzenie nazwy pliku
  2. nullglobMożna użyć opcji powłoki, aby uniknąć tej dodatkowej linii.
  3. „Jeśli ustawiono failglobopcję powłoki i nie znaleziono żadnych dopasowań, drukowany jest komunikat o błędzie i polecenie nie jest wykonywane.” (z podręcznika Bash powyżej)
  4. opcja powłoki globstar: „Jeśli jest ustawiony, wzorzec„ ** ”użyty w kontekście rozszerzenia nazwy pliku będzie pasował do wszystkich plików i zero lub więcej katalogów i podkatalogów. Jeśli po wzorcu występuje„ / ”, pasują tylko katalogi i podkatalogi.” patrz Podręcznik Bash, wbudowany Shopt
  5. Inne możliwości rozszerzania nazw: extglob, nocaseglob, dotglobi zmienna powłokiGLOBIGNORE

na wzorze 2:

  1. nazwy plików mogą zawierać spacje, tabulatory, spacje, znaki nowej linii, ... w celu bezpiecznego przetwarzania nazw plików w bezpieczny sposób, findprzy -print0użyciu: nazwa pliku jest drukowana ze wszystkimi znakami kontrolnymi i kończy się na NUL. zobacz także Gnu Findutils Manpage, Niebezpieczna obsługa nazw plików , bezpieczna obsługa nazw plików , nietypowe znaki w nazwach plików . David A. Wheeler poniżej zawiera szczegółowe omówienie tego tematu.

  2. Istnieje kilka możliwych wzorców przetwarzania wyników wyszukiwania w pętli while. Inni (kevin, David W.) pokazali, jak to zrobić za pomocą rur:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Gdy wypróbujesz ten fragment kodu, zobaczysz, że nie działa: files_foundjest zawsze „prawdziwy”, a kod zawsze będzie powtarzał „nie znaleziono plików”. Powód jest taki: każde polecenie potoku jest wykonywane w osobnej podpowłoce, więc zmienna zmienna w pętli (osobna podpowłoka) nie zmienia zmiennej w głównym skrypcie powłoki. Dlatego zalecam stosowanie podstawienia procesu jako „lepszego”, bardziej użytecznego, bardziej ogólnego wzorca.
    Zobacz : Ustawiam zmienne w pętli, która jest w potoku. Dlaczego znikają ... (z FAQ Grega Basha) w celu szczegółowej dyskusji na ten temat.

Dodatkowe referencje i źródła:

Michael Brux
źródło
8

(Zaktualizowano w celu uwzględnienia wyjątkowej poprawy prędkości @ Socowi)

Z każdym, $SHELLktóry obsługuje (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Gotowe.


Oryginalna odpowiedź (krótsza, ale wolniejsza):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
użytkownik569825
źródło
1
Powolny jak melasa (ponieważ uruchamia powłokę dla każdego pliku), ale to działa. +1
dawg
1
Zamiast tego \;możesz użyć, +aby przekazać jak najwięcej plików do jednego exec. Następnie użyj "$@"wewnątrz skryptu powłoki, aby przetworzyć wszystkie te parametry.
Socowi
3
W tym kodzie jest błąd. W pętli brakuje pierwszego wyniku. To dlatego, że $@pomija go, ponieważ zwykle jest to nazwa skryptu. Po prostu trzeba dodać dummyw między 'i {}tak można ją zastąpić nazwą skryptu, zapewniając wszystkie mecze są przetwarzane przez pętlę.
BCartolo,
Co jeśli potrzebuję innych zmiennych spoza nowo utworzonej powłoki?
Jodo,
OTHERVAR=foo find . -na.....powinien umożliwiać dostęp $OTHERVARz poziomu nowo utworzonej powłoki.
user569825,
6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
bmargulies
źródło
3
for x in $(find ...)zepsuje się dla każdej nazwy pliku z białymi spacjami. To samo dotyczy, find ... | xargschyba że użyjesz -print0i-0
glenn jackman
1
Użyj find . -name "*.txt -exec process_one {} ";"zamiast tego. Dlaczego powinniśmy używać xargs do zbierania wyników, które już mamy?
użytkownik nieznany
@userunknown Cóż, wszystko zależy od tego, co process_onejest. Jeśli jest to symbol zastępczy rzeczywistego polecenia , upewnij się, że zadziała (jeśli poprawisz literówkę i dodasz cudzysłowy zamykające po "*.txt). Ale jeśli process_onefunkcja jest zdefiniowana przez użytkownika, kod nie będzie działał.
toksalot
@toxalot: Tak, ale nie byłoby problemu z napisaniem funkcji w skrypcie do wywołania.
użytkownik nieznany
4

Możesz zapisać swoje finddane wyjściowe w tablicy, jeśli chcesz później je wykorzystać jako:

array=($(find . -name "*.txt"))

Teraz, aby wydrukować każdy element w nowym wierszu, możesz albo użyć foriteracji pętli do wszystkich elementów tablicy, albo możesz użyć instrukcji printf.

for i in ${array[@]};do echo $i; done

lub

printf '%s\n' "${array[@]}"

Możesz także użyć:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Spowoduje to wydrukowanie każdej nazwy pliku w nowym wierszu

Aby wydrukować dane findwyjściowe tylko w formie listy, możesz użyć jednej z następujących czynności:

find . -name "*.txt" -print 2>/dev/null

lub

find . -name "*.txt" -print | grep -v 'Permission denied'

Spowoduje to usunięcie komunikatów o błędach i podanie nazwy pliku tylko w nowym wierszu.

Jeśli chcesz zrobić coś z nazwami plików, dobrze jest przechowywać je w tablicy, w przeciwnym razie nie ma potrzeby zajmowania tego miejsca i możesz bezpośrednio wydrukować dane wyjściowe find.

Rakholiya Jenish
źródło
1
Pętla nad tablicą kończy się niepowodzeniem ze spacjami w nazwach plików.
EM0
Powinieneś usunąć tę odpowiedź. Nie działa ze spacjami w nazwach plików lub katalogach.
jww
4

Jeśli możesz założyć, że nazwy plików nie zawierają znaków nowej linii, możesz odczytać dane wyjściowe finddo tablicy Bash za pomocą następującego polecenia:

readarray -t x < <(find . -name '*.txt')

Uwaga:

  • -tpowoduje readarrayusunięcie nowych linii.
  • Nie będzie działać, jeśli readarrayjest w rurze, stąd podstawienie procesu.
  • readarray jest dostępny od wersji Bash 4.

Bash 4.4 i nowsze wersje obsługują również -dparametr określający ogranicznik. Użycie znaku null zamiast znaku nowej linii do określenia nazw plików działa również w rzadkich przypadkach, gdy nazwy plików zawierają znaki nowej linii:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraymożna również wywołać, jak w mapfileprzypadku tych samych opcji.

Odniesienie: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Seppo Enarvi
źródło
To najlepsza odpowiedź! Współpracuje z: * Spacjami w nazwach plików * Brak pasujących plików * exitpodczas zapętlania wyników
EM0
Nie działa jednak z wszystkimi możliwymi nazwami plików - w tym celu należy użyćreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

Lubię używać find, który jest najpierw przypisany do zmiennej, a IFS przełącza się na nową linię w następujący sposób:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Na wypadek gdybyś chciał powtórzyć więcej akcji na tym samym zestawie DANYCH i znaleźć na swoim serwerze bardzo wolno (wysokie wykorzystanie I / 0)

Paco
źródło
2

Możesz umieścić zwrócone nazwy plików findw tablicy takiej jak ta:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Teraz możesz po prostu przejść przez tablicę, aby uzyskać dostęp do poszczególnych elementów i robić z nimi, co chcesz.

Uwaga: jest bezpieczny dla białej przestrzeni.

Jahid
źródło
1
Z bash 4.4 lub wyższy można użyć jednego polecenia zamiast pętli: mapfile -t -d '' array < <(find ...). Ustawienie IFSnie jest konieczne mapfile.
Socowi
1

na podstawie innych odpowiedzi i komentarza @phk, używając fd # 3:
(co wciąż pozwala na użycie stdin wewnątrz pętli)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
źródło
-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Spowoduje to wyświetlenie listy plików i podanie szczegółowych informacji o atrybutach.

chetangb
źródło