Loop gennem filer med mellemrum i navnene? [duplikat]

Dette spørgsmål har allerede svar her :

Kommentarer

  • Jeg er uenig i, at dette ville være en duplikat. Det accepterede svar svarer på, hvordan man løber over filnavne med mellemrum; der har intet at gøre med " hvorfor løber over find ' s output dårlig praksis ". Jeg fandt dette spørgsmål (ikke det andet), fordi jeg har brug for at løbe over filnavne med mellemrum, som i: for fil i $ LIST_OF_FILES; gør … hvor $ LIST_OF_FILES ikke er resultatet af find; det ' er bare en liste med filnavne (adskilt af nye linjer).
  • @CarloWood – filnavne kan omfatte nye linjer, så dit spørgsmål er ret unikt: løber over en liste over filnavne, der kan indeholde mellemrum, men ikke nye linjer. Jeg tror, at du ' bliver nødt til at bruge IFS-teknikken for at indikere, at bruddet sker ved ' \ n '
  • @ Diagonwooah, jeg har aldrig indset, at filnavne må indeholde nye linjer. Jeg bruger mest (kun) linux / UNIX, og der er endda mellemrum sjældne; Jeg så bestemt aldrig i hele mit liv, at der blev brugt nye linjer: s. De kan lige så godt forbyde den imho.
  • @CarloWood – filnavne slutter med en null (' \ 0 ' , samme som ' '). Alt andet er acceptabelt.
  • @CarloWood Du skal huske, at folk stemmer først og læser andet …

Svar

Kort svar (tættest på dit svar, men håndterer mellemrum)

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 filnavne)

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 

Bedste svar (baseret 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 endnu bedre for at undgå at køre en sh pr. 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 skallen output af en kommando på mellemrum, faner og nye linjer
  2. Filnavne kan indeholde jokertegn, som ville blive udvidet
  3. Hvad hvis der er en mappe, hvis navn ender på *.csv?

1. Splitting kun på nye linjer

For at finde ud af, hvad der skal indstilles file til, skal skallen tage output af find og fortolke det på en eller anden måde, ellers ville file bare være hele output af find .

Skallen læser IFS -variablen, som er indstillet til <space><tab><newline> som standard.

Så ser det på hvert tegn i output fra find. Så snart det ser et tegn, der “s i IFS, mener det, at det markerer slutningen af filnavnet, så det indstiller file til hvilke tegn det så indtil nu og kører sløjfen. Derefter starter den, hvor den slap for at få det næste filnavn, og kører den næste sløjfe osv., indtil den når slutningen af output.

Så det gør dette effektivt:

for file in "zquery" "-" "abc" ... 

For at fortælle det kun at opdele input på nye linjer, skal du gøre

IFS=$"\n" 

før din for ... find kommando.

Det sætter IFS til en enkelt ny linje, så den kun opdeles på nye linjer og ikke mellemrum og faner.

Hvis du bruger sh eller dash i stedet for ksh93, bash eller zsh, skal du skrive IFS=$"\n" sådan i stedet:

IFS=" " 

Det er sandsynligvis nok for at få dit script til at fungere, men hvis du er interesseret i at håndtere andre hjørnesager korrekt, skal du læse videre …

2. Udvider $file uden jokertegn

Inde i sløjfen, hvor du gør

diff $file /some/other/path/$file 

skalen forsøger at udvide $file (igen!).

Den kunne indeholde mellemrum, men da vi allerede har indstillet IFS ovenfor, der vil ikke være et problem her.

Men det kunne også indeholde jokertegn som * eller ?, hvilket ville føre til uforudsigelig opførsel. (Tak til Gilles for at påpege dette.)

For at fortælle skallen ikke at udvide jokertegn skal du placere variablen i dobbelt anførselstegn, f.eks.

diff "$file" "/some/other/path/$file" 

Det samme problem kan også bide os i

for file in `find . -name "*.csv"` 

Hvis du f.eks. havde disse tre filer

file1.csv file2.csv *.csv 

(meget usandsynligt, men stadig muligt)

Det ville være som om du havde kørt

for file in file1.csv file2.csv *.csv 

som vil blive udvidet til

for file in file1.csv file2.csv *.csv file1.csv file2.csv 

forårsager file1.csv og file2.csv skal behandles to gange.

I stedet skal vi gø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 læser linjer fra standardinput, opdeler linjen i ord i henhold til IFS og gemmer dem i de variabelnavne, du angiver.

Her fortæller vi det ikke at opdele linjen i ord og at gemme linjen i $file.

Bemærk også, at er ændret til read line </dev/tty.

Dette skyldes, at inde i sløjfen kommer standardindgangen fra find via pipelinen.

Hvis vi bare gjorde read, ville det forbruge en del af eller hele et filnavn, og nogle filer blev sprunget over .

/dev/tty er den terminal, hvorfra brugeren kører scriptet fra. Bemærk, at dette vil forårsage en fejl, hvis scriptet køres via cron, men jeg antager, at dette ikke er vigtigt i dette tilfælde.

Hvad så hvis et filnavn indeholder nye linjer?

Vi kan klare det ved at ændre -print til -print0 og bruge read -d "" i slutningen af en 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 

Dette får find til at sætte en nullbyte i slutningen af hvert filnavn. Nulbyte er de eneste tegn, der ikke er tilladt i filnavne, så dette skal håndtere alle mulige filnavne, uanset hvor underligt.

For at få filnavnet på den anden side bruger vi IFS= read -r -d "".

Hvor vi brugte read ovenfor, brugte vi standardlinjeafgrænsningen for newline, men nu find bruger null som linjeafgrænser. I bash kan du ikke videregive et NUL-tegn i et argument til en kommando (selv indbyggede), men bash forstår -d "" som betydning NUL afgrænset . Så vi bruger -d "" til at gøre read brug den samme linieafgrænser som find. Bemærk, at -d $"\0" i øvrigt også fungerer, fordi bash ikke understøtter NUL-byte behandler det som den tomme streng.

For at være korrekt tilføjer vi også -r, som siger, at du ikke håndterer tilbageslag i specielt filnavne. Uden -r fjernes \<newline>, og \n konverteres til n.

En mere bærbar måde at skrive dette på, der ikke kræver bash eller zsh eller husker alle ovennævnte regler om nullbyte (igen takket være Gilles):

find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";" 

* 3. Springe over mapper, hvis navne ender på .csv

find . -name "*.csv" 

vil også matche mapper, der hedder something.csv.

For at undgå dette skal du tilføje -type f til 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åpeger, i begge disse eksempler er kommandoerne, der skal udføres for hver fil, køres i en subshell, så hvis du ændrer nogen variabler inden i loop, vil de blive glemt.

Hvis du har brug for at indstille variabler og have dem stadig indstillet i slutningen af sløjfen kan du omskrive den for at bruge proceserstatning som denne:

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" 

Bemærk, at hvis du prøver at kopiere og indsætte dette på kommandolinjen , read line forbruger echo "$i files processed", så kommandoen ikke bliver kørt.

For at undgå dette skal du kunne fjerne read line </dev/tty og sende resultatet til en personsøger som less.


NOTER

Jeg fjernede semikolonerne (;) inde i løkke. Du kan sætte dem tilbage, hvis du vil, men de er ikke nødvendige.

I disse dage er $(command) mere almindelig end `command`. Dette skyldes hovedsageligt, at det er lettere at skrive $(command1 $(command2)) end `command1 \`command2\``.

read char læser ikke rigtig et tegn.Den læser en hel linje, så jeg ændrede den til read line.

Kommentarer

  • sætter while i en pipeline kan oprette problemer med den oprettede subshell (variabler i loopblokken er ikke synlige efter f.eks. Kommandoen er afsluttet). Med bash ville jeg bruge omdirigering af input og proceserstatning: while read -r -d $'\0' file; do ...; done < <(find ... -print0)
  • Sikker eller ved hjælp af en heredoc: while read; do; done <<EOF "$(find)" EOF . Ikke så let at læse dog.
  • @glenn jackman: Jeg forsøgte at tilføje flere forklaringer lige nu. Gjorde jeg det bare bedre eller værre?
  • Du behøver ikke ' t har brug for IFS, -print0, while og read hvis du håndterer find fuldt ud, som vist nedenfor i min løsning.
  • Din første løsning kan klare ethvert tegn undtagen newline hvis du også slukker for globbing med set -f.

Svar

Dette script mislykkes, hvis et filnavn indeholder mellemrum eller shell-kugleformede tegn \[?*. find kommandoen udsender et filnavn pr. Linje. Derefter evalueres kommandosubstitutionen `find …` af skallen som følger:

  1. Udfør find kommandoen, tag fat i dens output.
  2. Del find output i separate ord. Ethvert mellemrumstegn er en ordseparator.
  3. Hvis det er et globmønster for hvert ord, skal du udvide det til listen over filer, det matcher.

F.eks. Antag, at der er tre filer i den aktuelle mappe, der hedder `foo* bar.csv, foo 1.txt og foo 2.txt.

  1. Kommandoen find returnerer ./foo* bar.csv.
  2. Skallen deler denne streng i mellemrummet og producerer to ord: ./foo* og bar.csv.
  3. Siden ./foo* indeholder en kugleformet metategn, den udvides til listen med matchende filer: ./foo 1.txt og ./foo 2.txt.
  4. Derfor udføres for -sløjfen successivt med ./foo 1.txt, ./foo 2.txt og bar.csv.

Du kan undgå de fleste problemer på dette tidspunkt ved at nedtone orddelingen og dreje ing af globbing. For at tone ned opdelingen af ord skal du indstille variablen IFS til et enkelt nyt linjetegn; på denne måde vil output fra find kun blive delt på nye linjer, og mellemrum forbliver. For at slå globbing fra skal du køre set -f. Så fungerer denne del af koden, så længe intet filnavn indeholder et nyt linjetegn.

IFS=" " set -f for file in $(find . -name "*.csv"); do … 

(Dette er ikke en del af dit problem, men jeg anbefaler at bruge $(…) over `…`. De har den samme betydning, men backquote-versionen har underlige citeringsregler.)

Der er et andet problem nedenfor: diff $file /some/other/path/$file skal være

diff "$file" "/some/other/path/$file" 

Ellers skal værdien af $file er opdelt i ord, og ordene behandles som globmønstre, som med kommandosubstitutio ovenfor. Hvis du skal huske en ting ved shell-programmering, skal du huske dette: Brug altid dobbelt anførselstegn omkring variable udvidelser ($foo) og kommandosubstitutioner ( $(bar)) , medmindre du ved, at du vil opdele. (Ovenfor vidste vi, at vi ville opdele find output i linjer.)

En pålidelig måde at ringe til find fortæller den, at den skal køre en kommando for hver fil, den finder:

find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";" 

I dette tilfælde er en anden tilgang at sammenligne de to mapper, selvom du skal ekskluderer eksplicit alle de “kedelige” filer.

diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path 

Kommentarer

  • I ' glemte jokertegn som en anden grund til at citere ordentligt. Tak! 🙂
  • i stedet for find -exec sh -c 'cmd 1; cmd 2' ";", skal du bruge find -exec cmd 1 {} ";" -exec cmd 2 {} ";", fordi skallen skal maskere parametrene, men find ikke ' t. I det særlige tilfælde her behøver ekko " $ 0 " ikke ' t at være et del af scriptet, bare tilføj -print efter ';'. Du har ikke ' ikke medtaget et spørgsmål for at fortsætte, men selv det kan gøres ved at finde, som vist nedenfor i min sjæl. 😉
  • @userunknown: Brug af {} som en substring af en parameter i find -exec er ikke bærbar, at ' hvorfor skallen er nødvendig.Jeg forstår ikke ' hvad du mener med “skallen skal maskere parametrene”; hvis det ' handler om at citere, er min løsning korrekt citeret. Du ' har ret i, at echo -delen kunne udføres af -print i stedet. -okdir er en ret nylig GNU-find-udvidelse, den ' er ikke tilgængelig overalt. Jeg inkluderede ikke ' ventetiden på at fortsætte, fordi jeg mener, at ekstremt dårlig brugergrænseflade og spørgeren let kan sætte read i shell-uddraget, han ønsker.
  • Citering er en form for maskering, er det ikke '? Jeg forstår ikke ' din bemærkning om, hvad der er bærbart, og hvad ikke. Dit eksempel (2. fra bunden) bruger -exec til at påkalde sh og bruger {} – så hvor er mit eksempel (ved siden af -okdir) mindre transportabel? find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
  • “Maskering” er ikke ' t almindelig terminologi i shell-litteratur, så du ' Jeg skal forklare, hvad du mener, hvis du vil blive forstået. Mit eksempel bruger {} kun en gang og i et separat argument; andre tilfælde (bruges to gange eller som understreng) er ikke bærbare. “Bærbar” betyder, at den ' fungerer på alle unix-systemer; en god retningslinje er POSIX / Single Unix-specifikationen .

Svar

Jeg er overrasket over at ikke se readarray nævnt. Det gør det meget let, når det bruges i kombination med <<< operator:

$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words| 

Brug af <<<"$expansion" -konstruktionen giver dig også mulighed for at opdele variabler, der indeholder nye linjer i arrays, som :

$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset 

readarray har været i Bash i årevis nu, så dette burde sandsynligvis være den kanoniske måde at gøre dette i Bash.

Svar

Afaik find har alt hvad du behøver.

find . -okdir diff {} /some/other/path/{} ";" 

find tager sig af at kalde programmerne sikkert. -okdir vil bede dig om diff (er du sikker på ja / nej).

Ingen shell involveret, ingen globbing, jokere, pi, pa, po.

Som sidenote: Hvis du kombinerer find med for / while / do / xargs, i de fleste tilfælde, y du gør det forkert. 🙂

Kommentarer

  • Tak for svaret. Hvorfor laver du det forkert, hvis du kombinerer find med for / while / do / xargs?
  • Find allerede iterater over et undersæt af filer. De fleste mennesker, der dukker op med spørgsmål, kan bare bruge en af handlingerne (-ok (dir) -exec (dir), -delete) i kombination med "; " eller + (senere til parallelopkald). Hovedårsagen til at gøre det er, at du ikke ' ikke behøver at fikle rundt med filparametre og maskere dem til skallen. Ikke så vigtigt: Du har ikke brug for ' t nye processer hele tiden, mindre hukommelse, mere hastighed. kortere program.
  • Ikke her for at knuse din ånd, men sammenlign: time find -type f -exec cat "{}" \; med time find -type f -print0 | xargs -0 -I stuff cat stuff. xargs -versionen var hurtigere med 11 sekunder, når 10000 tomme filer blev behandlet. Vær forsigtig, når du hævder, at det i de fleste tilfælde er forkert at kombinere find med andre hjælpeprogrammer. -print0 og -0 er der for at håndtere mellemrum i filnavne ved at bruge en nulbyte som artikelseparator snarere end et mellemrum.
  • @JonathanKomar: Din find / exec kommando tog 11,7 s på mit system med 10.000 filer, xargs version 9.7 s, time find -type f -exec cat {} + som foreslået i min tidligere kommentar tog 0,1 s. Bemærk den subtile forskel mellem " det er forkert " og " dig ' gør det forkert ", især når det er dekoreret med en smilie. Har du for eksempel gjort det forkert? 😉 BTW, mellemrum i filnavnet er ikke noget problem for ovenstående kommando og finder generelt. Lastkult-programmør? Og forresten er det fint at kombinere find med andre værktøjer, bare xargs er for det meste overflødigt.
  • @userunknown Jeg forklarede, hvordan min kode handler om pladser for eftertiden (uddannelse af fremtidige seere), og var ikke antyde, at din kode ikke gør det. + til parallelle opkald er meget hurtig, som du nævnte. Jeg vil ikke sige lastkult-programmør, fordi denne evne til at bruge xargs på denne måde kommer til nytte ved flere lejligheder. Jeg er mere enig med Unix-filosofien: gør en ting og gør det godt (brug programmer separat eller i kombination for at få et job udført). find går en fin linje der.

Svar

Gennemse alle filer ( ethvert specialtegn inkluderet) med Find helt sikkert (se linket til dokumentation):

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

  • Tak for at nævne -d ''. Jeg indså ikke ' at $'\0' var det samme som '', men det ser ud til være. God løsning.
  • Jeg kan godt lide afkobling af find og while, tak.

Svar

Jeg er overrasket over, at ingen nævnte den åbenlyse zsh løsning her endnu:

for file (**/*.csv(ND.)) { do-something-with $file } 

((D) for også at inkludere skjulte filer, (N) for at undgå fejlen, hvis der ikke er noget match, (.) for at begrænse til almindelige filer.)

bash4.3 og ovenfor understøtter det nu også delvist:

shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done 

Svar

Filnavne med mellemrum i dem ligner flere navne på kommandolinjen, hvis de ” ikke citeret. Hvis din fil hedder “Hello World.txt”, udvides diff-linjen til:

diff Hello World.txt /some/other/path/Hello World.txt 

, der ligner fire filnavne. Bare sæt citater omkring argumenterne:

diff "$file" "/some/other/path/$file" 

Kommentarer

  • Dette hjælper, men det gør det ikke ' t løser mit problem. Jeg ser stadig tilfælde, hvor filen opdeles i flere tokens.
  • Dette svar er vildledende. Problemet er kommandoen for file in `find . -name "*.csv"`. Hvis der er en fil med navnet Hello World.csv, indstilles file til ./Hello World.csv. Citering af $file vandt ' t hjælp.

Svar

Dobbelt citering er din ven.

diff "$file" "/some/other/path/$file" 

Ellers bliver variablenes indhold opdelt i ord.

Kommentarer

  • Dette er vildledende. Problemet er kommandoen for file in `find . -name "*.csv"`. Hvis der er en fil, der hedder Hello World.csv, file indstilles til ./Hello og derefter til World.csv. Citering $file vandt ' t hjælp.

Svar

Med bash4 kan du også bruge den indbyggede mapfile-funktion til at indstille en matrix, der indeholder hver linje og gentage på denne matrix.

$ 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

Mellemrummene i værdierne kan undgås ved så simpelt til loopkonstruktion

for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done 

ls -l root / somedir c indeholder min fil med mellemrum

Output over min fil med mellemrum

for at undgå denne output, enkel løsning (bemærk de dobbelte citater)

for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done 

output min fil med mellemrum

prøvet på bash

Kommentarer

  • “Loop gennem filer ”- det er hvad spørgsmålet siger. Din løsning sender hele ls -l output på én gang . Det svarer effektivt til echo "CHECKSTR `ls -l /root/somedir`".

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *