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:
- Som standard deler skallen output af en kommando på mellemrum, faner og nye linjer
- Filnavne kan indeholde jokertegn, som ville blive udvidet
- 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
ogread
hvis du håndtererfind
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:
- Udfør
find
kommandoen, tag fat i dens output. - Del
find
output i separate ord. Ethvert mellemrumstegn er en ordseparator. - 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
.
- Kommandoen
find
returnerer./foo* bar.csv
. - Skallen deler denne streng i mellemrummet og producerer to ord:
./foo*
ogbar.csv
. - Siden
./foo*
indeholder en kugleformet metategn, den udvides til listen med matchende filer:./foo 1.txt
og./foo 2.txt
. - Derfor udføres
for
-sløjfen successivt med./foo 1.txt
,./foo 2.txt
ogbar.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 brugefind -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 ifind -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, atecho
-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ætteread
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 "{}" \;
medtime 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 kombinerefind
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 brugexargs
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 navnetHello World.csv
, indstillesfile
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 hedderHello World.csv
,file
indstilles til./Hello
og derefter tilWorld.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 tilecho "CHECKSTR `ls -l /root/somedir`"
.