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