Løper du gjennom filer med mellomrom i navnene? [duplikat]

Dette spørsmålet har allerede svar her :

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:

  1. Som standard deler skallet utdataene fra en kommando i mellomrom, faner og nye linjer
  2. Filnavn kan inneholde jokertegn som ville bli utvidet
  3. 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 og read hvis du håndterer find 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:

  1. Utfør find -kommandoen, ta tak i utgangen.
  2. Del opp find -utgangen i separate ord. Ethvert hvitt mellomrom er en ordskiller.
  3. 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.

  1. find -kommandoen returnerer ./foo* bar.csv.
  2. Skallet deler denne strengen i rommet, og produserer to ord: ./foo* og bar.csv.
  3. Siden ./foo* inneholder en globerende metategn, den utvides til listen over samsvarende filer: ./foo 1.txt og ./foo 2.txt.
  4. Derfor blir for sløyfen utført suksessivt med ./foo 1.txt, ./foo 2.txt og bar.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 bruke find -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 i find -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 at echo -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 sette read 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 "{}" \; med time 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 å kombinere find 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 å bruke xargs 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 kalt Hello World.csv, vil file 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 heter Hello World.csv, file blir satt til ./Hello og deretter til World.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 effektivt echo "CHECKSTR `ls -l /root/somedir`".

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *