Kommentarer
- Jeg er uenig i at dette ville være et duplikat. Det aksepterte svaret svarer på hvordan man løper over filnavn med mellomrom; som ikke har noe med " å gjøre, hvorfor løper det å finne ' s utgang dårlig praksis ". Jeg fant dette spørsmålet (ikke det andre) fordi jeg må løpe over filnavn med mellomrom, som i: for fil i $ LIST_OF_FILES; gjør … hvor $ LIST_OF_FILES ikke er resultatet av finne; det ' er bare en liste over filnavn (atskilt med nye linjer).
- @CarloWood – filnavn kan inneholde nye linjer, så spørsmålet ditt er ganske unikt: å løpe en liste over filnavn som kan inneholde mellomrom, men ikke nye linjer. Jeg tror du ' du må bruke IFS-teknikken, for å indikere at bruddet skjer ved ' \ n '
- @ Diagon-woah, jeg har aldri innsett at filnavn har lov til å inneholde nye linjer. Jeg bruker stort sett (bare) linux / UNIX, og der er til og med mellomrom sjeldne; Jeg så absolutt aldri hele livet mitt at nye linjer ble brukt: s. De kan like godt forby den imhoen.
- @CarloWood – filnavn ender på null (' \ 0 ' , samme som ' '). Alt annet er akseptabelt.
- @CarloWood Du må huske at folk stemmer først og leser andre …
Svar
Kort svar (nærmest svaret ditt, men håndterer mellomrom)
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"
Bedre svar (håndterer også jokertegn og nye linjer i filnavn)
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
Beste svaret (basert på Gilles » svar )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Eller enda bedre, for å unngå å kjøre en sh
per fil:
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 {} +
Langt svar
Du har tre problemer:
- Som standard deler skallet utdataene fra en kommando i mellomrom, faner og nye linjer
- Filnavn kan inneholde jokertegn som ville bli utvidet
- Hva om det er en katalog hvis navnet ender på
*.csv
?
1. Splitting bare på nye linjer
For å finne ut hva du skal angi file
til, må skallet ta utdataene av find
og tolker det på en eller annen måte, ellers ville file
bare være hele utgangen av find
.
Skallet leser IFS
-variabelen, som er satt til <space><tab><newline>
som standard.
Så ser det på hvert tegn i utdata fra find
. Så snart det ser et tegn som «s i IFS
, tror det at det markerer slutten på filnavnet, så det setter file
til hvilke tegn den så til nå og kjører sløyfen. Deretter starter den der den slapp for å få neste filnavn, og kjører neste sløyfe osv. til den når slutten av utdata.
Så det gjør dette effektivt:
for file in "zquery" "-" "abc" ...
For å fortelle det å bare dele inndata på nye linjer, må du gjøre
IFS=$"\n"
før for ... find
-kommandoen.
Som setter IFS
til enkelt ny linje, slik at den bare deles på nye linjer, og ikke mellomrom og faner også.
Hvis du bruker sh
eller dash
i stedet for ksh93
, bash
eller zsh
, må du skrive IFS=$"\n"
som dette i stedet:
IFS=" "
Det er nok nok for å få skriptet til å fungere, men hvis du er interessert i å håndtere andre hjørnesaker riktig, les videre …
2. Utvider $file
uten jokertegn
Inne i løkken der du gjør
diff $file /some/other/path/$file
skallet prøver å utvide $file
(igjen!).
Det kan inneholde mellomrom, men siden vi allerede har satt IFS
ovenfor, det vil ikke være et problem her.
Men det kan også inneholde jokertegn som *
eller ?
, noe som vil føre til uforutsigbar oppførsel. (Takk til Gilles for at du påpekte dette.)
For å be skallet om ikke å utvide jokertegn, legger du variabelen i doble anførselstegn, f.eks.
diff "$file" "/some/other/path/$file"
Det samme problemet kan også bite oss i
for file in `find . -name "*.csv"`
Hvis du for eksempel hadde disse tre filene
file1.csv file2.csv *.csv
(veldig usannsynlig, men fortsatt mulig)
Det ville være som om du hadde løpt
for file in file1.csv file2.csv *.csv
som utvides til
for file in file1.csv file2.csv *.csv file1.csv file2.csv
forårsaker file1.csv
og file2.csv
skal behandles to ganger.
I stedet må vi gjøre
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
leser linjer fra standardinndata, deler linjen i ord i henhold til IFS
og lagrer dem i variabelnavnene du angir.
Her forteller vi det ikke å dele linjen i ord, og å lagre linjen i $file
.
Vær også oppmerksom på at har endret seg til read line </dev/tty
.
Dette er fordi inne i sløyfen kommer standardinngangen fra find
via rørledningen.
Hvis vi bare gjorde read
, ville det fortære en del av eller hele filnavnet, og noen filer vil bli hoppet over .
/dev/tty
er terminalen der brukeren kjører skriptet fra. Merk at dette vil forårsake en feil hvis skriptet kjøres via cron, men jeg antar at dette ikke er viktig i dette tilfellet.
Hva om et filnavn inneholder nye linjer?
Vi kan takle det ved å endre -print
til -print0
og bruke read -d ""
på slutten av en rørledning:
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
Dette gjør at find
setter en nullbyte på slutten av hvert filnavn. Null byte er de eneste tegnene som ikke er tillatt i filnavn, så dette skal håndtere alle mulige filnavn, uansett hvor rart.
For å få filnavnet på den andre siden bruker vi IFS= read -r -d ""
.
Der vi brukte read
ovenfor, brukte vi standardlinjeavgrenseren til newline, men nå, find
bruker null som linjeskille. I bash
kan du ikke sende et NUL-tegn i et argument til en kommando (selv innebygde), men bash
forstår -d ""
som betyr NUL avgrenset . Så vi bruker -d ""
for å lage read
bruk den samme linjeavgrenseren som find
. Merk at -d $"\0"
, forresten, også fungerer, fordi bash
ikke støtter NUL-byte, behandler den som den tomme strengen.
For å være riktig, legger vi også til -r
, som sier at du ikke håndterer tilbakeslag filnavn spesielt. Uten -r
fjernes \<newline>
, og \n
konverteres til n
.
En mer bærbar måte å skrive dette på som ikke krever bash
eller zsh
eller husker alle de ovennevnte reglene om nullbyte (igjen, takk til Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Hopp over kataloger hvis navn ender på .csv
find . -name "*.csv"
vil også matche kataloger som heter something.csv
.
For å unngå dette, legg til -type f
i find
-kommandoen.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Som glenn jackman påpeker, i begge disse eksemplene er kommandoene som skal utføres for hver fil kjøres i en subshell, så hvis du endrer noen variabler inne i løkken, vil de bli glemt.
Hvis du trenger å sette variabler og ha dem fortsatt satt på slutten av løkken, kan du skrive den om for å bruke prosessubstitusjon slik:
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"
Merk at hvis du prøver å kopiere og lime inn dette på kommandolinjen , read line
vil forbruke echo "$i files processed"
, slik at kommandoen ikke blir kjørt.
For å unngå dette, må du kunne fjerne read line </dev/tty
og sende resultatet til en personsøker som less
.
MERKNADER
Jeg fjernet semikolonene (;
) inne i Løkke. Du kan sette dem tilbake hvis du vil, men de er ikke nødvendige.
I disse dager er $(command)
vanligere enn `command`
. Dette er hovedsakelig fordi det er lettere å skrive $(command1 $(command2))
enn `command1 \`command2\``
.
read char
leser ikke virkelig et tegn.Den leser en hel linje, så jeg endret den til read line
.
Kommentarer
- putter
while
i en rørledning kan skape problemer med at subshell er opprettet (variabler i sløyfeblokken er ikke synlige etter at kommandoen er fullført for eksempel). Med bash vil jeg bruke omadressering og prosessubstitusjon:while read -r -d $'\0' file; do ...; done < <(find ... -print0)
- Jada, eller ved hjelp av en heredoc:
while read; do; done <<EOF "$(find)" EOF
. Ikke så lett å lese. - @glenn jackman: Jeg prøvde å legge til mer forklaring akkurat nå. Gjorde jeg det bare bedre eller verre?
- Du trenger ikke ' t trenger
IFS, -print0, while
ogread
hvis du håndtererfind
til fulle, som vist nedenfor i løsningen min. - Den første løsningen din takler alle tegn unntatt ny linje hvis du også slår av globbing med
set -f
.
Svar
Dette skriptet mislykkes hvis filnavnet inneholder mellomrom eller skjellklottegn \[?*
. Kommandoen find
sender ut ett filnavn per linje. Deretter blir kommandosubstitusjonen `find …`
evaluert av skallet som følger:
- Utfør
find
-kommandoen, ta tak i utgangen. - Del opp
find
-utgangen i separate ord. Ethvert hvitt mellomrom er en ordskiller. - Hvis det er et globmønster for hvert ord, utvider du det til listen over filer det samsvarer med.
For eksempel, antar at det er tre filer i den nåværende katalogen, kalt `foo* bar.csv
, foo 1.txt
og foo 2.txt
.
-
find
-kommandoen returnerer./foo* bar.csv
. - Skallet deler denne strengen i rommet, og produserer to ord:
./foo*
ogbar.csv
. - Siden
./foo*
inneholder en globerende metategn, den utvides til listen over samsvarende filer:./foo 1.txt
og./foo 2.txt
. - Derfor blir
for
sløyfen utført suksessivt med./foo 1.txt
,./foo 2.txt
ogbar.csv
.
Du kan unngå de fleste problemer på dette stadiet ved å tonere ned splitting og snu ing av globbing. For å tone ned orddeling, sett IFS
-variabelen til et enkelt nylinjetegn; på denne måten vil utdata fra find
bare bli delt på nye linjer og mellomrom blir værende. For å slå av globbing, kjør set -f
. Da vil denne delen av koden fungere så lenge ingen filnavn inneholder et nytt linjetegn.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Dette er ikke en del av problemet ditt, men jeg anbefaler å bruke $(…)
over `…`
. De har samme betydning, men backquote-versjonen har rare siteringsregler.)
Det er et annet problem nedenfor: diff $file /some/other/path/$file
skal være
diff "$file" "/some/other/path/$file"
Ellers skal verdien av $file
er delt inn i ord og ordene blir behandlet som globmønstre, som med kommandosubstitutio ovenfor. Hvis du må huske en ting om skallprogrammering, husk dette: Bruk alltid doble anførselstegn rundt variable utvidelser ($foo
) og kommandosubstitusjoner ( $(bar)
) , med mindre du vet at du vil dele. (Ovenfor visste vi at vi ville dele find
-utgangen i linjer.)
En pålitelig måte å ringe find
forteller den at den skal kjøre en kommando for hver fil den finner:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
I dette tilfellet er en annen tilnærming å sammenligne de to katalogene, selv om du må ekskluderer eksplisitt alle de «kjedelige» filene.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Kommentarer
- I ' glemte jokertegn som en annen grunn til å sitere ordentlig. Takk! 🙂
- i stedet for
find -exec sh -c 'cmd 1; cmd 2' ";"
, bør du brukefind -exec cmd 1 {} ";" -exec cmd 2 {} ";"
, fordi skallet må maskere parametrene, men finn ikke ' t. I det spesielle tilfellet her trenger ikke ekko " $ 0 " ' å være et del av skriptet, bare legg til -print etter';'
. Du inkluderte ikke ' et spørsmål for å fortsette, men selv det kan gjøres ved å finne, som vist nedenfor i min sjel. 😉 - @userunknown: Bruken av
{}
som en delstreng av en parameter ifind -exec
er ikke bærbar, at ' er hvorfor skallet er nødvendig.Jeg forstår ikke ' hva du mener med «skallet trenger å maskere parametrene»; hvis det ' handler om å sitere, er løsningen min riktig sitert. Du ' har rett i atecho
-delen kan utføres av-print
i stedet.-okdir
er en ganske nylig GNU-finn-utvidelse, den ' er ikke tilgjengelig overalt. Jeg inkluderte ikke ' ventetiden på å fortsette fordi jeg anser at ekstremt dårlig brukergrensesnitt og spøreren lett kan setteread
i skallutdraget hvis han vil. - Sitering er en form for maskering, er det ikke '? Jeg forstår ikke ' din kommentar om hva som er bærbart og hva ikke. Eksempelet ditt (andre fra bunnen) bruker -exec til å påkalle
sh
og bruker{}
– så hvor er eksemplet mitt (ved siden av -okdir) mindre bærbar?find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
- “Masking” er ikke ' t vanlig terminologi i skalllitteraturen, så du ' Jeg må forklare hva du mener hvis du vil bli forstått. Eksemplet mitt bruker
{}
bare en gang og i et eget argument; andre tilfeller (brukes to ganger eller som underlag) er ikke bærbare. «Bærbar» betyr at den ' fungerer på alle unix-systemer; en god retningslinje er POSIX / Single Unix-spesifikasjonen .
Svar
Jeg er overrasket over å ikke se readarray
nevnt. Det gjør dette veldig enkelt når det brukes i kombinasjon med <<<
operator:
$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words|
Ved å bruke <<<"$expansion"
-konstruksjonen kan du også dele variabler som inneholder nye linjer i matriser, som :
$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset
readarray
har vært i Bash i mange år nå, så dette burde nok være den kanoniske måten å gjøre dette i Bash.
Svar
Afaik find har alt du trenger.
find . -okdir diff {} /some/other/path/{} ";"
find tar seg av å ringe programmene på en forsvarlig måte. -okdir vil be deg om diff (er du sikker på at ja / nei).
Ingen skall involvert, ingen globbing, jokere, pi, pa, po.
Som sidenotat: Hvis du kombinerer find med for / while / do / xargs, i de fleste tilfeller, y du gjør det galt. 🙂
Kommentarer
- Takk for svaret. Hvorfor gjør du det galt hvis du kombinerer find med for / while / do / xargs?
- Finn allerede iterater over en delmengde av filer. De fleste som dukker opp med spørsmål, kan bare bruke en av handlingene (-ok (dir) -exec (dir), -delete) i kombinasjon med "; " eller + (senere for parallell påkalling). Hovedårsaken til det er at du ikke ' ikke trenger å fikle rundt med filparametere og maskere dem for skallet. Ikke så viktig: Du trenger ' t nye prosesser hele tiden, mindre minne, mer hastighet. kortere program.
- Ikke her for å knuse din ånd, men sammenlign:
time find -type f -exec cat "{}" \;
medtime find -type f -print0 | xargs -0 -I stuff cat stuff
.xargs
-versjonen var raskere med 11 sekunder ved behandling av 10000 tomme filer. Vær forsiktig når du hevder at det i de fleste tilfeller er feil å kombinerefind
med andre verktøy.-print0
og-0
er der for å håndtere mellomrom i filnavnene ved å bruke en nullbyte som vareseparator i stedet for et mellomrom. - @JonathanKomar: Din find / exec-kommando tok 11,7 s på systemet mitt med 10.000 filer, xargs-versjonen 9.7 s,
time find -type f -exec cat {} +
som foreslått i min forrige kommentar tok 0,1 s. Legg merke til den subtile forskjellen mellom " det er feil " og " deg ' gjør det galt ", spesielt når det er dekorert med en smilie. Gjorde du for eksempel det galt? 😉 BTW, mellomrom i filnavnet er ikke noe problem for kommandoen ovenfor og finner generelt. Lastekultprogrammerer? Og forresten, det er greit å kombinere finne med andre verktøy, bare xargs er for det meste overflødig. - @userunknown Jeg forklarte hvordan koden min håndterer rom for ettertiden (utdanning av fremtidige seere), og var ikke antyde at koden din ikke gjør det.
+
for parallelle samtaler er veldig rask, som du nevnte. Jeg vil ikke si lastekultprogrammerer, fordi denne evnen til å brukexargs
på denne måten kommer til nytte ved flere anledninger. Jeg er mer enig i Unix-filosofien: gjør en ting og gjør det bra (bruk programmer hver for seg eller i kombinasjon for å få en jobb ferdig).find
går en fin linje der.
Svar
Bla gjennom alle filer ( alle spesialtegn inkludert) med helt trygt funn (se lenken for dokumentasjon):
exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d "" -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done
Kommentarer
- Takk for at du nevner
-d ''
. Jeg skjønte ikke ' at$'\0'
var det samme som''
være. God løsning også. - Jeg liker avkoblingen av finne og mens, takk.
Svar
Jeg er overrasket over at ingen nevnte den åpenbare zsh
løsningen her ennå:
for file (**/*.csv(ND.)) { do-something-with $file }
((D)
for også å inkludere skjulte filer, (N)
for å unngå feilen hvis det ikke samsvarer, (.)
for å begrense til vanlige filer.)
bash4.3
og over støtter det nå også delvis:
shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done
Svar
Filnavn med mellomrom ser ut som flere navn på kommandolinjen hvis de » ikke sitert. Hvis filen din heter «Hello World.txt», utvides diff-linjen til:
diff Hello World.txt /some/other/path/Hello World.txt
som ser ut som fire filnavn. Bare sett sitater rundt argumentene:
diff "$file" "/some/other/path/$file"
Kommentarer
- Dette hjelper, men det gjør ikke ' t løse problemet mitt. Jeg ser fremdeles tilfeller der filen blir delt opp i flere tokens.
- Dette svaret er misvisende. Problemet er kommandoen
for file in `find . -name "*.csv"`
. Hvis det er en fil kaltHello World.csv
, vilfile
settes til./Hello
World.csv
. Sitat$file
vant ' t hjelp.
Svar
Dobbelt sitering er din venn.
diff "$file" "/some/other/path/$file"
Ellers blir variabelen innhold orddelt.
Kommentarer
- Dette er misvisende. Problemet er
for file in `find . -name "*.csv"`
-kommandoen. Hvis det er en fil som heterHello World.csv
,file
blir satt til./Hello
og deretter tilWorld.csv
. Sitat$file
vant ' t hjelp.
Svar
Med bash4 kan du også bruke den innebygde mapfile-funksjonen til å angi en matrise som inneholder hver linje og gjenta på denne matrisen.
$ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1
Svar
Mellomromene i verdiene kan unngås ved så enkelt for loopkonstruksjon
for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done
ls -l root / somedir c inneholder filen min med mellomrom
Utdata over filen min med mellomrom
for å unngå denne utdata, enkel løsning (legg merke til de dobbelte anførselstegnene)
for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done
send ut filen min med mellomrom
prøvd på bash
Kommentarer
- “Looping through files ”- det er det spørsmålet sier. Løsningen din sender ut hele
ls -l
utgangen på en gang . Det tilsvarer effektivtecho "CHECKSTR `ls -l /root/somedir`"
.