Jak wykonać akcję na wszystkich plikach z określonym rozszerzeniem w podfolderach w elegancki sposób?

18

Mój obecny najlepszy zakład to:

for i in $(find . -name *.jpg); do echo $i; done

Problem: nie obsługuje spacji w nazwach plików.

Uwaga: Chciałbym też graficznie to zrobić, na przykład polecenie „drzewo”.

Vorac
źródło
Czy nie możesz po prostu umieścić cytatów $i? Robię to i działa dobrze. Czy jest coś nie tak z tym podejściem?
Umar Farooq Khawaja

Odpowiedzi:

26

Kanoniczny sposób to zrobić

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

(wymienić \;z +przejść więcej niż jeden plik echona raz)

lub (specyficzne dla GNU, chociaż niektóre BSD również je teraz mają):

find . -name '*.jpg' -print0 | xargs -r0 echo

zsh:

for i (**/*.jpg(D)) echo $i
styczeń
źródło
2
Jeśli używasz xargs -0 , powinieneś dopasować go do find -0
itsbruce
1
@ MichaelKjörling, brak cytowania {} nie ma znaczenia. {} jest rozszerzone przez find, a nie powłokę. Ulepszeniem byłoby nieużywanie, echoktóre rozszerza sekwencje specjalne\b (przynajmniej zgodne z Unixem echo).
Stéphane Chazelas
1
@sch Jesteś pewien? Strona findman pokazuje find . -type f -exec file '{}' \;w EXAMPLESsekcji. Może niektóre pociski specjalnie potraktują aparat ortodontyczny.
QuasarDonkey
1
Cytaty nie szkodzą, ale nie mają znaczenia. Wszystko '{}', \{\}, {}, "{}", '{'}są rozszerzane przez powłokę (cokolwiek podobnego Bourne shell) do jednego argumentu do stwierdzenia, że jest dwa znaki „{” i „}”. Zamień „znajdź” na „znajdź echo” lub printf '<%s>\n' findjeśli chcesz dwukrotnie sprawdzić.
Stéphane Chazelas,
@QuasarDonkey Patrz unix.stackexchange.com/questions/8647/…
Gilles „SO- przestań być zły”
2

Podano już lepsze odpowiedzi.

Pamiętaj jednak, że spacje nie są jedynym problemem w podanym kodzie. Problem stanowią także tabulatory, znaki nowej linii i znaki wieloznaczne.

Z

IFS='
'
set -f
for i in $(find . -name '*.jpg'); do echo "$i"; done

Wtedy problem stanowią tylko znaki nowej linii.

Stéphane Chazelas
źródło
1

To, o czym wspomniałeś, jest jednym z podstawowych problemów, z którymi borykają się ludzie, gdy próbują odczytać nazwy plików. Czasami ludzie z ograniczoną wiedzą i nieporozumieniem co do struktury plików i folderów zwykle zapominają, że „ W UNIX Wszystko jest plikiem ”. Dlatego nie rozumieją, że muszą również obsługiwać spacje, ponieważ nazwa pliku może składać się z 2 lub więcej słów ze spacjami.

Rozwiązanie: Więc jednym z bardziej znanych sposobów na zrobienie tego jest czyste czytanie .

Przeczytamy wszystkie obecne pliki i zachowamy je w zmiennej, następnym razem, gdy wykonamy pożądane przetwarzanie, będziemy musieli po prostu zachować tę zmienną w cudzysłowach, co pozwoli zachować nazwy plików ze spacjami. Jest to jeden z podstawowych sposobów, aby to zrobić, jednak inne odpowiedzi udzielone przez innych ludzi tutaj również działają.

PISMO:

 find /path/to/files/ -iname "*jpg" | \
  while read I; do
   cp -v --parent "$I" /backup/dir/
  done

Tutaj czytam pliki, podając ścieżkę do nich, i cokolwiek czytam, przechowuję je w zmiennej I, którą zacytuję w późniejszym czasie, przetwarzając ją, aby zachować spacje, aby poprawnie ją przetworzyć.

Mam nadzieję, że to ci w jakiś sposób pomoże.

Mroczny rycerz
źródło
1
„wszystko jest plikiem” odnosi się do czegoś innego. Twoje rozwiązanie jest specyficzne dla GUN i nie radzi sobie z odwrotnym ukośnikiem lub znakami nowej linii w nazwach plików. Pętle „podczas odczytu” w powłokach (IMO) są często oznaką złej praktyki pisania skryptów w powłokach.
Stéphane Chazelas,
@sch: huh, i myślałem, że jest w porządku. Dziękujemy za zwrócenie na to uwagi. Prawdopodobnie muszę na to więcej spojrzeć.
The Dark Knight
przepraszam, miałem na myśli „GNU”, a nie „GUN” powyżej (po raz pierwszy zdaję sobie sprawę, że to anagramy BTW ...)
Stéphane Chazelas,
1

Po prostu za pomocą findi bash:

find . -name '*.jpg' -exec bash -c '
    echo "treating file $1 in bash, from path : ${1%/*}" 
' -- {} \;

W ten sposób używamy $1w bash, podobnie jak w podstawowym skrypcie, który otwiera ładne perspektywy do wykonywania wszelkich zaawansowanych (lub nie) zadań.

Wydajniejszy sposób (aby uniknąć konieczności rozpoczynania nowego basha dla każdego pliku):

find . -name '*.jpg' -exec bash -c 'for i do
    echo "treating file $i in bash, from path : ${i%/*}" 
  done' -- {} +
Gilles Quenot
źródło
0

W najnowszej wersji bash możesz użyć globstaropcji:

shopt -s globstar
for file in **/*.jpg ; do
    echo "$file"
done

W przypadku prostych akcji możesz nawet całkowicie pominąć pętlę:

echo **/*.jpg
choroba
źródło
Chociaż działa to w Zsh (skąd pochodzi ta funkcja) oraz w ksh93 (z set -G), nie należy go używać w bash, ponieważ wersja bash jest zasadniczo zepsuta (podobnie jak GNU grep -r), ponieważ schodzi do dowiązań symbolicznych do katalogów (odpowiednik find -Llub zsh ***/*.jpg). Zauważ też, że w przeciwieństwie do znajdowania, będzie on opuszczał pliki kropkowe i nie opadał do kropek (domyślnie).
Stéphane Chazelas,