Tworzenie skryptu BASH `for` do obsługi nazw plików ze spacjami (lub obejściem)

12

Podczas gdy używam BASH od kilku lat, moje doświadczenie ze skryptowaniem BASH jest stosunkowo ograniczone.

Mój kod jest jak poniżej. Powinien pobrać całą strukturę katalogów z bieżącego katalogu i zreplikować ją $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

Problem polega na tym, że oto przykład mojej struktury plików:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Zwróć uwagę na spacje: -S I for pobiera parametry słowo po słowie, więc wynik mojego skryptu wygląda mniej więcej tak:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Ale potrzebuję go, aby pobrać całe nazwy plików (jedna linia na raz) z wyjścia find. Próbowałem też robić find umieszczaj podwójne cudzysłowy wokół każdej nazwy pliku. Ale to nie pomaga.

for DIR in `find . -type d -printf "\"%P\"\040"`

A wyjście z tą zmienioną linią:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Teraz potrzebuję jakiegoś sposobu, aby przejść przez to w ten sposób, ponieważ chcę też uruchomić bardziej skomplikowane polecenie gstreamer na każdym pliku w następującej podobnej strukturze. Jak mam to robić?

Edytować: Potrzebuję struktury kodu, która pozwoli mi uruchomić wiele linii kodu dla każdego katalogu / pliku / pętli. Przepraszam, jeśli byłam niejasna.

Rozwiązanie: Początkowo próbowałem:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

W większości działało to dobrze. Jednak później odkryłem, że ponieważ potok powoduje pętlę while działającą w podpowłoce, później wszystkie zmienne ustawione w pętli były niedostępne, co utrudniało implementację licznika błędów. Moje ostateczne rozwiązanie (od ta odpowiedź na SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

To później pozwoliło mi warunkowo zwiększyć zmienne w pętli, które pozostałyby dostępne później w skrypcie.

Samuel Jaeschke
źródło
Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko
To prawda, nie moje preferencje. Aby usunąć spacje, musisz najpierw obsługiwać pliki ze spacjami;)
Samuel Jaeschke
1
W rzeczywistości nazwy plików powinny umożliwiać spacje. Pozwoliłbym na wszystko, ale / i niedrukowalne znaki. Ale wszystko jest dozwolone z wyjątkiem / i \0 więc musisz im pozwolić.
Kevin Panko

Odpowiedzi:

11

Musisz potokować find w a while pętla.

find ... | while read -r dir
do
    something with "$dir"
done

Ponadto nie musisz używać -printf w tym przypadku.

Możesz zrobić ten dowód przeciwko plikom z nowymi znakami w nazwach, jeśli chcesz, używając separatora nullbyte (który jest jedynym znakiem, który nie może pojawić się w ścieżce pliku * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Znajdziesz również użycie $() zamiast backticks być bardziej wszechstronnym i łatwiejszym. Można je znacznie łatwiej zagnieżdżać, a cytowanie można zrobić znacznie łatwiej. Ten wymyślony przykład ilustruje te punkty:

echo "$(echo "$(echo "hello")")"

Spróbuj to zrobić za pomocą backticks.

Dennis Williamson
źródło
2
Również raczej niż "$dir", lepiej jest użyć "${dir}" - łatwo jest odróżnić nazwę $ {dir} od $ {dirname}, ale $ dirname można interpretować w dowolny sposób.
James Polley
Ważne jest tutaj to read czyta całą linię do ${dir}, więc IFS nie ma znaczenia.
James Polley
1
Dzięki za znalezienie literówki $ / ". Nawiasy nie są konieczne, jeśli po nazwie zmiennej nie ma nic.
Dennis Williamson
4
Spowoduje to obsługę nazw ścieżek ze spacjami (U + 0020), ale nadal nie będzie w stanie poprawnie obsługiwać nazw ścieżek za pomocą kanałów (U + 000A). wolę find … -print0 | xargs -0 … ponieważ separator, którego używa, odpowiada dokładnie jedynemu znakowi, który nie jest dozwolony w ścieżkach POSIX: NUL (U + 0000).
Chris Johnsen
2
Idealny! Właśnie tego szukałem. Nigdy nie przyszło mi do głowy, że możesz się do tego przyzwyczaić while. @Chris Johnsen: Prawda, ale nawet programy do zgrywania muzyki nie mają tendencji do umieszczania kanałów w nazwach plików. A jeśli tak, chcę wiedzieć (tzn. Coś pójdzie nie tak) i natychmiast się ich pozbyć ...
Samuel Jaeschke
7

Widzieć ta odpowiedź Napisałem kilka dni temu przykład skryptu, który obsługuje nazwy plików ze spacjami.

Istnieje jednak nieco bardziej skomplikowany (ale bardziej zwięzły) sposób na osiągnięcie tego, co próbujesz zrobić:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0 mówi find, aby oddzielił argumenty o wartości null; -0 do xargs mówi mu, by oczekiwał argumentów oddzielonych przez null. Oznacza to, że dobrze obsługuje przestrzenie.

-I {} mówi xargs, aby zastąpił ciąg {} z nazwą pliku. Oznacza to również, że w wierszu poleceń powinna być używana tylko jedna nazwa pliku (xargs będzie zwykle wypychać tyle, ile zmieści się w linii)

Reszta powinna być oczywista.

James Polley
źródło
Sugestia Dennisa Williamsona jest jednak (oprócz literówek) o wiele bardziej czytelna, a więc preferowana niemal pod każdym względem.
James Polley
Działa dla mkdir, ale przepraszam, że powinienem być bardziej przejrzysty - chcę uruchomić serię poleceń dla każdego pliku. Widzisz, dla mojej podobnej procedury później chcę wygenerować wyjściową nazwę pliku na podstawie nazwy pliku wejściowego (co obejmuje usunięcie rozszerzenia .ogg i dodanie pliku .mp3), a następnie użycie tych wielu zmiennych w moim pakiecie podczas wywoływania gst-launch.
Samuel Jaeschke
5

Problem, który napotykasz, to instrukcja for odpowiadająca na znalezione jako osobne argumenty. Ogranicznik przestrzeni. Musisz użyć zmiennej IFS bash, aby nie rozdzielać przestrzeni.

Tutaj jest połączyć to wyjaśnia, jak to zrobić.

Wewnętrzna zmienna IFS

Jednym ze sposobów na rozwiązanie tego problemu jest zmiana wewnętrznej zmiennej Bash IFS (Internal Field Separator) tak, aby dzieliła pola na inne niż domyślne białe znaki (spacja, tabulator, nowa linia), w tym przypadku przecinek.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Ustaw swoje znalezisko na wyjście ogranicznika pola po% P i odpowiednio ustaw IFS. Wybrałem średnik, ponieważ jest mało prawdopodobne, aby go znaleźć w twoich nazwach plików.

Inną alternatywą jest wywołanie mkdir ze znaleziska bezpośrednio przez -exec czy możesz całkowicie pominąć pętlę for. Jeśli nie musisz wykonywać żadnych dodatkowych analiz.

Darren Hall
źródło
Co jeśli nazwa pliku zawiera IFS? Następnie musisz wybrać inny. Ale co, jeśli ...
Dennis Williamson
3
Możesz wybrać / w POSIX i : na systemach plików DOS. Istnieją nielegalne znaki dla różnych systemów plików, które można wybrać dla IFS. Wszystko bardziej skomplikowane i lepiej jest używać perla.
Darren Hall
2
Problem z użyciem / polega na tym, że jest to separator katalogu i find zwraca nazwy plików ze ścieżkami zawierającymi ukośnik. Spróbuj zmienić średnik w skrypcie na ukośnik, a echo wydrukuje katalog i nazwę pliku w osobnych wierszach.
Dennis Williamson
To również wygląda całkiem pożytecznie. Poszłam z fajką do while opcja, ale wygląda to również całkiem dobrze. Tak, w mojej podobnej strukturze później musiałem wykonać dalsze parsowanie. (Nazwa pliku wejściowego to .ogg, który zostanie przekazany jako filesrc w potoku gst, ale zostanie wygenerowany odpowiednik kończący się na .mp3 w katalogu wyjściowym, a także przekazany do potoku jako filesink, i oczywiście trzeba to zrobić dla każdego pliku, razem z niektórymi echo do użytkownika.)
Samuel Jaeschke
4

Jeśli treść twojej pętli jest więcej niż jednym poleceniem, można z niej skorzystać xargs sterować skryptem powłoki:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Pamiętaj, aby dołączyć kreskę końcową (lub inne „słowo”), jeśli powłoka jest odmiany Bourne / POSIX (jest używana do ustawienia 0 $ w skrypcie powłoki). Należy również zachować ostrożność przy cytowaniu, ponieważ skrypt powłoki jest zapisywany w cudzysłowionym łańcuchu zamiast bezpośrednio w monicie.

Chris Johnsen
źródło
Kolejna ciekawa koncepcja. Dzięki - jestem pewien, że znajdę dla tego zastosowanie później :)
Samuel Jaeschke
1

w zaktualizowanym pytaniu

mkdir -p \"${OUTPATH}${DIR}\"

to powinno być

mkdir -p "${OUTPATH}${DIR}"
user23307
źródło
Dzięki. Naprawiony. Czytał także na FILENAME zamiast DIR - kopiuj-wklej: P
Samuel Jaeschke
1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'
Vouze
źródło
0

lub aby uczynić całość mniej skomplikowaną:

% rsync -av --include='*/' --exclude='*' SRC DST

to replikuje strukturę katalogów SRC na DST.

akira
źródło
Nie, potrzebuję takiej iteracyjnej struktury, która pozwala mi uruchamiać wiele linii kodu dla każdego pliku. „Teraz potrzebuję jakiegoś sposobu, aby przejść przez to w ten sposób, ponieważ chcę również uruchomić bardziej skomplikowane polecenie z udziałem gstreamer na każdym pliku w następującej podobnej strukturze. Przepraszam, jeśli byłam niejasna.
Samuel Jaeschke
Polecenie, które podałem rozwiązuje problem, o który prosiłeś, nie ma znaczenia, czy jest to tylko część większego „potoku” po twojej stronie. dla kogoś, kto ma problem opisany w pytaniu, podejście rsync będzie działać. więc nie ma potrzeby przepraszać za potencjalną niejasność :)
akira
Tak. Nie, mam na myśli, że używałbym podobnego while ... do ... done struktura później, aby wykonać podobne przetwarzanie ze find, co wymagałoby uruchomienia kilku wierszy kodu na każdym pliku (zmodyfikuj ciąg, echo, gst-launch itp.) i rsync nie osiągnie tego. Dlatego określiłem, że muszę być w stanie uruchomić bardziej skomplikowany zestaw poleceń w podobnej strukturze. Mój skrypt używa tej struktury pętli dwa razy, więc na pytanie zamieściłem ten z mniejszą ilością crud na środku.
Samuel Jaeschke
0

Jeśli masz GNU Parallel http: // www.gnu.org/software/parallel/ zainstalowany, możesz to zrobić:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Obejrzyj film wprowadzający do GNU Parallel, aby dowiedzieć się więcej: http://www.youtube.com/watch?v=OpaiGYxkSuQ

Ole Tange
źródło