Krótka odpowiedź (najbliższa Twojej odpowiedzi, ale zawiera spacje)
OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS"
Lepsza odpowiedź (obsługuje również symbole wieloznaczne i znaki nowej linii w nazwach plików)
find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
Najlepsza odpowiedź (na podstawie Gilles ” answer )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Lub nawet lepiej, aby uniknąć uruchamiania jednego sh
na plik:
find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} +
Długa odpowiedź
Masz trzy problemy:
- Domyślnie powłoka dzieli wyjście polecenia na spacje, tabulatory i znaki nowej linii
- Nazwy plików mogą zawierać symbole wieloznaczne, które zostanie rozwinięty
- Co się stanie, jeśli istnieje katalog, którego nazwa kończy się na
*.csv
?
1. Dzielenie tylko na nowej linii
Aby dowiedzieć się, na co ustawić file
, powłoka musi pobrać dane wyjściowe find
i jakoś zinterpretować, w przeciwnym razie file
będzie po prostu całym wynikiem find
.
Powłoka czyta zmienną IFS
, która jest domyślnie ustawiona na <space><tab><newline>
.
Następnie sprawdza każdy znak w wyniku find
. Gdy tylko zobaczy dowolny znak znajdujący się „w IFS
, myśli, że oznacza on koniec nazwy pliku, więc ustawia file
do dowolnych znaków, które widział do tej pory i uruchamia pętlę. Następnie zaczyna się od miejsca, w którym zostało przerwane, aby pobrać nazwę następnego pliku i uruchamia następną pętlę itd., aż osiągnie koniec wyniku.
Więc to skutecznie robi to:
for file in "zquery" "-" "abc" ...
Aby nakazać mu dzielenie wejścia tylko na znaki nowej linii, musisz zrobić
IFS=$"\n"
przed poleceniem for ... find
.
To ustawia IFS
na pojedyncza nowa linia, więc dzieli się tylko na nowe linie, a nie spacje i tabulatory.
Jeśli używasz sh
lub dash
zamiast ksh93
, bash
lub zsh
, musisz napisać IFS=$"\n"
w ten sposób:
IFS=" "
To prawdopodobnie wystarczy aby skrypt działał, ale jeśli chcesz poprawnie obsłużyć inne przypadki narożne, czytaj dalej …
2. Rozwijanie $file
bez symboli wieloznacznych
Wewnątrz pętli, gdzie robisz
diff $file /some/other/path/$file
powłoka próbuje rozwinąć $file
(ponownie!).
Może zawierać spacje, ale ponieważ ustawiliśmy już IFS
powyżej, to nie będzie tutaj problemem.
Ale może też zawierać symbole wieloznaczne, takie jak *
lub ?
, co prowadziłoby do nieprzewidywalnego zachowania. (Dzięki Gillesowi za wskazanie tego.)
Aby powiedzieć powłoce, aby nie rozwijała znaków wieloznacznych, umieść zmienną w podwójnych cudzysłowach, np.
diff "$file" "/some/other/path/$file"
Ten sam problem może nas również ugryźć
for file in `find . -name "*.csv"`
Na przykład, gdybyś miał te trzy pliki
file1.csv file2.csv *.csv
(bardzo mało prawdopodobne, ale nadal możliwe)
To byłoby tak, jakbyś uruchomił
for file in file1.csv file2.csv *.csv
który zostanie rozwinięty do
for file in file1.csv file2.csv *.csv file1.csv file2.csv
powodując file1.csv
i file2.csv
do przetworzenia dwukrotnie.
Zamiast tego musimy zrobić
find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
read
czyta wiersze ze standardowego wejścia, dzieli je na słowa zgodnie z IFS
i zapisuje je w nazwach zmiennych, które określisz.
Tutaj mówimy o tym nie dzielić wiersza na słowa i przechowywać wiersz w $file
.
Należy również pamiętać, że zmieniło się na read line </dev/tty
.
Dzieje się tak, ponieważ wewnątrz pętli standardowe wejście pochodzi z find
przez potok.
Gdybyśmy właśnie wykonali read
, zużywałoby to część lub całość nazwy pliku, a niektóre pliki zostałyby pominięte .
/dev/tty
to terminal, z którego użytkownik uruchamia skrypt. Zauważ, że spowoduje to błąd, jeśli skrypt zostanie uruchomiony przez cron, ale zakładam, że w tym przypadku nie ma to znaczenia.
A co, jeśli nazwa pliku zawiera znaki nowej linii?
Możemy sobie z tym poradzić, zmieniając -print
na -print0
i używając read -d ""
na końcu pipeline:
find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done
To sprawia, że find
umieszcza pusty bajt na końcu nazwy każdego pliku. Puste bajty to jedyne znaki niedozwolone w nazwach plików, więc powinno obsługiwać wszystkie możliwe nazwy plików, nieważne jak dziwne.
Aby uzyskać nazwę pliku z drugiej strony, używamy IFS= read -r -d ""
.
Tam, gdzie powyżej użyliśmy read
, użyliśmy domyślnego ogranicznika linii nowej linii, ale teraz find
używa null jako separatora linii. W bash
nie można „przekazać znaku NUL w argumencie do polecenia (nawet wbudowanych), ale bash
rozumie -d ""
w znaczeniu rozdzielany NUL . Dlatego używamy -d ""
, aby utworzyć read
użyj tego samego separatora linii co find
. Pamiętaj, że -d $"\0"
, nawiasem mówiąc, też działa, ponieważ bash
brak obsługi bajtów NUL traktuje go jako pusty ciąg.
Aby być poprawnym, dodajemy również -r
, który mówi, że nie obsługuj odwrotnych ukośników w nazwy plików specjalnie. Na przykład bez -r
, \<newline>
są usuwane, a \n
jest konwertowane na n
.
Bardziej przenośny sposób pisania, który nie wymaga bash
ani zsh
lub pamiętając o wszystkich powyższych zasadach dotyczących bajtów zerowych (ponownie dzięki Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Pomijanie katalogów, których nazwy kończą się na .csv
find . -name "*.csv"
będą również pasować do katalogów o nazwie something.csv
.
Aby tego uniknąć, dodaj -type f
do polecenia find
.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Jak wskazuje glenn jackman , w obu tych przykładach polecenia do wykonania dla każdego pliku to są uruchamiane w podpowłoce, więc jeśli zmienisz jakiekolwiek zmienne wewnątrz pętli, zostaną one zapomniane.
Jeśli musisz ustawić zmienne i nadal je ustawiać na końcu pętli możesz go przepisać tak, aby używał podstawiania procesów w następujący sposób:
i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed"
Zauważ, że jeśli spróbujesz skopiować i wkleić to z linii poleceń , read line
zużyje echo "$i files processed"
, więc to polecenie nie zostanie uruchomione.
Aby tego uniknąć, może usunąć read line </dev/tty
i wysłać wynik na pager, taki jak less
.
UWAGI
Usunąłem średniki (;
) wewnątrz pętla. Jeśli chcesz, możesz je odłożyć z powrotem, ale nie są one potrzebne.
Obecnie $(command)
jest bardziej powszechne niż `command`
. Dzieje się tak głównie dlatego, że łatwiej jest napisać $(command1 $(command2))
niż `command1 \`command2\``
.
read char
tak naprawdę nie czyta znaku.Czyta cały wiersz, więc zmieniłem go na read line
.
Komentarze
Ten skrypt kończy się niepowodzeniem, jeśli jakakolwiek nazwa pliku zawiera spacje lub znaki globalizacji powłoki \[?*
. Polecenie find
wyświetla jedną nazwę pliku w każdym wierszu. Następnie podstawienie polecenia `find …`
jest oceniane przez powłokę w następujący sposób:
- Wykonaj polecenie
find
, pobierz jego wynik.
- Podziel wyjście
find
na osobne słowa. Każdy biały znak jest separatorem wyrazów.
- Dla każdego słowa, jeśli jest to wzorzec globowania, rozwiń go do listy plików, do których pasuje.
Na przykład: załóżmy, że w bieżącym katalogu znajdują się trzy pliki o nazwach `foo* bar.csv
, foo 1.txt
i foo 2.txt
.
- Polecenie
find
zwraca ./foo* bar.csv
.
- Powłoka dzieli ten ciąg w miejscu, tworząc dwa słowa:
./foo*
i bar.csv
.
- Ponieważ
./foo*
zawiera globujący metaznak, jest on rozszerzany do listy pasujących plików: ./foo 1.txt
i ./foo 2.txt
.
- Dlatego pętla
for
jest wykonywana kolejno z ./foo 1.txt
, ./foo 2.txt
i bar.csv
.
Większość problemów na tym etapie można uniknąć, stonując podział na słowa i obracając wypuszczanie globbingu. Aby złagodzić podział na słowa, ustaw zmienną IFS
na pojedynczy znak nowej linii; w ten sposób wynik find
zostanie podzielony tylko na znaki nowej linii, a spacje pozostaną. Aby wyłączyć globowanie, uruchom set -f
. Wtedy ta część kodu będzie działać tak długo, jak długo żadna nazwa pliku nie zawiera znaku nowej linii.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(To nie jest część twojego problemu, ale ja zalecamy używanie $(…)
zamiast `…`
. Mają to samo znaczenie, ale wersja z cudzysłowami ma dziwne zasady cytowania.)
Jest jeszcze jeden problem poniżej: diff $file /some/other/path/$file
powinno być
diff "$file" "/some/other/path/$file"
W przeciwnym razie wartość $file
jest podzielone na słowa, a słowa są traktowane jako wzorce glob, tak jak w powyższym poleceniu substutio. Jeśli musisz pamiętać o programowaniu powłoki, pamiętaj o tym: zawsze używaj podwójnych cudzysłowów wokół rozszerzeń zmiennych ($foo
) i podstawień poleceń ( $(bar)
) , chyba że wiesz, że chcesz podzielić. (Powyżej wiedzieliśmy, że chcemy podzielić wynik find
na wiersze).
Niezawodny sposób wywoływania find
mówi mu, aby uruchomił polecenie dla każdego znalezionego pliku:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
W tym przypadku innym podejściem jest porównanie dwóch katalogów, chociaż musisz jawnie wyklucz wszystkie „nudne” pliki.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Komentarze