Răspuns scurt (cel mai apropiat de răspunsul dvs., dar gestionează spații)
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"
Răspuns mai bun (gestionează și metacaractere și linii noi în numele fișierelor)
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
Cel mai bun răspuns (bazat pe Gilles ” răspuns )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Sau chiar mai bine, pentru a evita rularea unuia sh
per fișier:
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 {} +
Răspuns lung
Aveți trei probleme:
- În mod implicit, shell-ul împarte ieșirea unei comenzi pe spații, file și linii noi
- Numele fișierelor pot conține caractere wildcard care s-ar extinde
- Ce se întâmplă dacă există un director al cărui nume se termină cu
*.csv
?
1. Împărțirea numai pe linii noi
Pentru a afla la ce să setați file
, shell-ul trebuie să ia ieșirea din find
și interpretați-l cumva, altfel file
ar fi doar întreaga ieșire a find
.
În shell se citește variabila IFS
, care este setată la <space><tab><newline>
în mod implicit.
Apoi, se uită la fiecare caracter din ieșirea din find
. De îndată ce vede orice caracter care „este în IFS
, consideră că marchează sfârșitul numelui fișierului, așa că setează file
la orice caractere a văzut până acum și rulează bucla. Apoi începe de unde a rămas pentru a obține următorul nume de fișier și rulează următoarea buclă etc. etc. până ajunge la sfârșitul ieșirii.
Deci, face efectiv acest lucru:
for file in "zquery" "-" "abc" ...
Pentru a-i spune să împartă intrarea doar pe linii noi, trebuie să faceți
IFS=$"\n"
înainte de comanda for ... find
.
Aceasta setează IFS
la singură linie nouă, deci se împarte doar pe linii noi, nu și spații și file.
Dacă utilizați sh
sau dash
în loc de ksh93
, bash
sau zsh
, trebuie să scrieți IFS=$"\n"
așa în schimb:
IFS=" "
Probabil că este suficient pentru ca scriptul dvs. să funcționeze, dar dacă „sunteți interesat să gestionați corect alte cazuri de colț, citiți mai departe …
2. Extinderea $file
fără metacaracter
În bucla unde faceți
diff $file /some/other/path/$file
shell-ul încearcă să extindă $file
(din nou!).
Ar putea conține spații, dar din moment ce am setat deja IFS
de mai sus, acest lucru nu va fi o problemă aici.
Dar ar putea conține și caractere wildcard precum *
sau ?
, ceea ce ar duce la un comportament imprevizibil. (Mulțumim lui Gilles pentru că a subliniat acest lucru.)
Pentru a spune shell-ului să nu extindă caracterele wildcard, puneți variabila în ghilimele duble, de ex.
diff "$file" "/some/other/path/$file"
Aceeași problemă ne-ar putea mușca și în
for file in `find . -name "*.csv"`
De exemplu, dacă ați avea aceste trei fișiere
file1.csv file2.csv *.csv
(foarte puțin probabil, dar totuși posibil)
Ar fi ca și cum ai fi executat
for file in file1.csv file2.csv *.csv
care va fi extins la
for file in file1.csv file2.csv *.csv file1.csv file2.csv
cauzând file1.csv
și file2.csv
să fie procesate de două ori.
În schimb, trebuie să facem
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
citește linii din intrarea standard, împarte linia în cuvinte în conformitate cu IFS
și le stochează în numele variabilelor pe care le specificați.
Aici, îi spunem să nu împărțiți linia în cuvinte și să stocați linia în $file
.
De asemenea, rețineți că s-a schimbat în read line </dev/tty
.
Acest lucru se datorează faptului că în interiorul buclei, intrarea standard provine de la find
prin conductă.
Dacă am face doar read
, ar fi consumat parțial sau integral un nume de fișier, iar unele fișiere ar fi omise .
/dev/tty
este terminalul de la care utilizatorul rulează scriptul. Rețineți că acest lucru va cauza o eroare dacă scriptul este rulat prin cron, dar presupun că acest lucru nu este important în acest caz.
Atunci, ce se întâmplă dacă un nume de fișier conține linii noi?
Ne putem ocupa de asta schimbând -print
în -print0
și folosind read -d ""
la sfârșitul unui 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
Acest lucru face ca find
să pună un octet nul la sfârșitul fiecărui nume de fișier. Octii nul sunt singurele caractere care nu sunt permise în numele fișierelor, deci aceasta ar trebui să gestioneze toate numele posibile ale fișierelor, oricât de ciudat ar fi.
Pentru a obține numele fișierului de cealaltă parte, folosim IFS= read -r -d ""
.
Acolo unde am folosit read
de mai sus, am folosit delimitatorul de linie implicit al newline, dar acum, find
folosește nul ca delimitator de linie. În bash
, nu puteți transmite un caracter NUL într-un argument unei comenzi (chiar și cele încorporate), dar bash
înțelege -d ""
ca semnificație NUL delimitat . Deci, folosim -d ""
pentru a face read
utilizați același delimitator de linie ca find
. Rețineți că -d $"\0"
, de altfel, funcționează la fel, deoarece bash
neacceptând octeții NUL îl tratează ca șirul gol.
Pentru a fi corect, adăugăm și -r
, care spune că nu se ocupă de backslashes în nume de fișiere special. De exemplu, fără -r
, \<newline>
sunt eliminate și \n
este convertit în n
.
Un mod mai portabil de a scrie acest lucru, care nu necesită bash
sau zsh
sau amintind toate regulile de mai sus despre octeți nuli (din nou, mulțumită lui Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Ignorarea directoarelor a căror numele se termină cu .csv
find . -name "*.csv"
se vor potrivi și cu directoarele numite something.csv
.
Pentru a evita acest lucru, adăugați -type f
la comanda 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 {} ";"
După cum subliniază glenn jackman , în ambele exemple, comenzile de executat pentru fiecare fișier sunt fiind rulat într-un subshell, deci dacă modificați orice variabilă din buclă, acestea vor fi uitate.
Dacă trebuie să setați variabile și să le setați în continuare la sfârșitul buclei, îl puteți rescrie pentru a utiliza înlocuirea procesului astfel:
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"
Rețineți că, dacă încercați să copiați și să lipiți acest lucru pe linia de comandă , read line
va consuma echo "$i files processed"
, astfel încât comanda să nu fie executată.
Pentru a evita acest lucru, ar putea să elimine read line </dev/tty
și să trimită rezultatul la un pager precum less
.
NOTE
Am eliminat punctele și virgulele (;
) din interiorul buclă. Le puteți pune înapoi dacă doriți, dar nu sunt necesare.
În zilele noastre, $(command)
este mai frecvent decât `command`
. Acest lucru se datorează în principal faptului că este mai ușor de scris $(command1 $(command2))
decât `command1 \`command2\``
.
read char
nu citește cu adevărat un personaj.Se citește un rând întreg, așa că l-am schimbat în read line
.
Comentarii
Acest script eșuează în cazul în care un nume de fișier conține spații sau caractere glob shell \[?*
. Comanda find
afișează un nume de fișier pe linie. Apoi, substituirea comenzii `find …`
este evaluată de shell după cum urmează:
- Executați comanda
find
, apucați ieșirea.
- Împărțiți ieșirea
find
în cuvinte separate. Orice caracter al spațiului alb este un separator de cuvinte.
- Pentru fiecare cuvânt, dacă este un model glob, extindeți-l la lista de fișiere cu care se potrivește.
De exemplu, să presupunem că există trei fișiere în directorul curent, numite `foo* bar.csv
, foo 1.txt
și foo 2.txt
.
- Comanda
find
returnează ./foo* bar.csv
.
- Coaja împarte acest șir în spațiu, producând două cuvinte:
./foo*
și bar.csv
.
- De la
./foo*
conține un metacaracter global, este extins la lista de fișiere potrivite: ./foo 1.txt
și ./foo 2.txt
.
- Prin urmare, bucla
for
se execută succesiv cu ./foo 1.txt
, ./foo 2.txt
și bar.csv
.
Puteți evita majoritatea problemelor în acest stadiu prin reducerea împărțirii cuvintelor și rotirea scăpând de globuri. Pentru a atenua împărțirea cuvintelor, setați variabila IFS
la un singur caracter de linie nouă; în acest fel, ieșirea find
va fi împărțită numai la linii noi și spațiile vor rămâne. Pentru a dezactiva blocarea globului, rulați set -f
. Apoi, această parte a codului va funcționa atâta timp cât niciun nume de fișier nu conține un caracter de linie nouă.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Aceasta nu face parte din problema dvs., dar eu recomandăm să folosiți $(…)
peste `…`
. Au același sens, dar versiunea de ghidare înapoi are reguli de citare ciudate.)
Mai jos este o altă problemă: diff $file /some/other/path/$file
ar trebui să fie
diff "$file" "/some/other/path/$file"
În caz contrar, valoarea $file
este împărțit în cuvinte, iar cuvintele sunt tratate ca modele glob, ca la comanda substitutio de mai sus. Dacă trebuie să vă amintiți un lucru despre programarea shell, amintiți-vă acest lucru: utilizați întotdeauna ghilimele duble în jurul expansiunilor variabile ($foo
) și înlocuirile comenzilor ( $(bar)
) , cu excepția cazului în care știți că doriți să vă împărțiți. (Mai sus, știam că dorim să împărțim ieșirea find
în linii.)
Un mod fiabil de a apela find
îi spune să ruleze o comandă pentru fiecare fișier pe care îl găsește:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
În acest caz, o altă abordare este de a compara cele două directoare, deși trebuie să excludeți în mod explicit toate fișierele „plictisitoare”.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Comentarii