Kommentarer
- Jag håller inte med om att detta skulle vara en duplikat. Det accepterade svaret svarar på hur man går över filnamn med mellanslag; som inte har något att göra med " varför slingrar sig hitta ' s utgång dålig praxis ". Jag hittade den här frågan (inte den andra) eftersom jag måste slinga över filnamn med mellanslag, som i: för fil i $ LIST_OF_FILES; gör … där $ LIST_OF_FILES inte är resultatet av sökningen; det ' är bara en lista med filnamn (åtskilda av nya rader).
- @CarloWood – filnamn kan innehålla nya rader, så din fråga är ganska unik: looping över en lista med filnamn som kan innehålla mellanslag men inte nya rader. Jag tror att du ' kommer att behöva använda IFS-tekniken för att indikera att pausen inträffar vid ' \ n '
- @ Diagonwoah, jag insåg aldrig att filnamn får innehålla nya rader. Jag använder mestadels (endast) linux / UNIX och där är även mellanslag sällsynta; Jag såg verkligen aldrig under hela mitt liv att nya linjer användes: s. De kan lika gärna förbjuda den imhoen.
- @CarloWood – filnamn slutar med en null (' \ 0 ' , samma som ' '). Allt annat är acceptabelt.
- @CarloWood Du måste komma ihåg att folk röstar först och läser andra …
Svar
Kort svar (närmast svaret men hanterar mellanslag)
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"
Bättre svar (hanterar även jokertecken och nya rader i filnamn)
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
Bästa svaret (baserat på Gilles ” svara )
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 ännu bättre, för att undvika att köra 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 {} +
Långt svar
Du har tre problem:
- Som standard delar skalet utdata från ett kommando i mellanslag, flikar och nya rader
- Filnamn kan innehålla jokertecken som skulle utvidgas
- Vad händer om det finns en katalog vars namn slutar på
*.csv
?
1. Dela bara på nya rader
För att ta reda på vad file
ska ställas in måste skalet ta utdata av find
och tolka det på något sätt, annars skulle file
bara vara hela utdata från find
.
Skalet läser IFS
variabeln, som är inställd på <space><tab><newline>
som standard.
Sedan ser det på varje tecken i utdata från find
. Så snart den ser något tecken som ”s i IFS
, tror det att det markerar slutet på filnamnet, så det sätter file
till de tecken som den såg tills nu och kör slingan. Sedan börjar den där den slutade för att få nästa filnamn och kör nästa slinga etc. tills den når slutet av utdata.
Så det gör detta effektivt:
for file in "zquery" "-" "abc" ...
För att säga att det bara ska dela in inmatningen på nya rader måste du göra
IFS=$"\n"
före ditt for ... find
-kommando.
Det sätter IFS
till en enda ny rad, så den delas bara på nya rader, och inte mellanrum och flikar också.
Om du använder sh
eller dash
istället för ksh93
, bash
eller zsh
, måste du skriva IFS=$"\n"
så här istället:
IFS=" "
Det räcker nog för att få ditt skript att fungera, men om du är intresserad av att hantera andra hörnfall korrekt, läs vidare …
2. Expanderar $file
utan jokertecken
Inuti slingan där du gör
diff $file /some/other/path/$file
skalet försöker expandera $file
(igen!).
Det kan innehålla mellanslag, men eftersom vi redan har ställt in IFS
ovan, det kommer inte att vara ett problem här.
Men det kan också innehålla jokertecken som *
eller ?
, vilket skulle leda till oförutsägbart beteende. (Tack till Gilles för att han påpekade detta.)
För att säga till skalet att inte expandera jokertecken, placera variabeln i dubbla citat, t.ex.
diff "$file" "/some/other/path/$file"
Samma problem kan också bita oss i
for file in `find . -name "*.csv"`
Om du till exempel hade dessa tre filer
file1.csv file2.csv *.csv
(mycket osannolikt, men ändå möjligt)
Det skulle vara som om du hade kört
for file in file1.csv file2.csv *.csv
som utvidgas till
for file in file1.csv file2.csv *.csv file1.csv file2.csv
vilket orsakar file1.csv
och file2.csv
ska behandlas två gånger.
Istället måste vi göra
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 rader från standardinmatning, delar upp raden i ord enligt IFS
och lagrar dem i de variabelnamn som du anger.
Här berättar vi det att inte dela upp raden i ord och att spara raden i $file
.
Observera också att har ändrats till read line </dev/tty
.
Detta beror på att inuti slingan kommer standardingången från find
via rörledningen.
Om vi bara gjorde read
skulle det ta bort en del eller hela filnamnet och vissa filer skulle hoppas över .
/dev/tty
är terminalen som användaren kör skriptet från. Observera att detta kommer att orsaka ett fel om skriptet körs via cron, men jag antar att detta inte är viktigt i det här fallet.
Vad händer om ett filnamn innehåller nya rader?
Vi kan hantera det genom att ändra -print
till -print0
och använda read -d ""
i slutet av 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
Detta gör att find
sätter en nullbyte i slutet av varje filnamn. Nollbyte är de enda tecknen som inte är tillåtna i filnamn, så detta bör hantera alla möjliga filnamn, oavsett hur konstigt.
För att få filnamnet på andra sidan använder vi IFS= read -r -d ""
.
Där vi använde read
ovan använde vi standardlinjens avgränsare för newline, men nu find
använder null som radavgränsare. I bash
kan du inte skicka ett NUL-tecken i ett argument till ett kommando (även inbyggda), men bash
förstår -d ""
som betyder NUL avgränsad . Så vi använder -d ""
för att göra read
använd samma radavgränsare som find
. Observera att -d $"\0"
fungerar för övrigt också eftersom bash
stöder inte NUL-byte behandlar det som den tomma strängen.
För att vara korrekt lägger vi till -r
, som säger att du inte hanterar snedstreck i filnamn speciellt. Utan -r
tas till exempel \<newline>
bort och \n
omvandlas till n
.
Ett mer bärbart sätt att skriva detta som inte kräver bash
eller zsh
eller komma ihåg alla ovanstående regler om nullbyte (igen, tack vare Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Hoppar över kataloger vars namn slutar på .csv
find . -name "*.csv"
kommer också att matcha kataloger som heter something.csv
.
För att undvika detta, lägg till -type f
till find
-kommandot.
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åpekar, i båda dessa exempel är kommandona att utföra för varje fil körs i en subshell, så om du ändrar variabler inuti slingan kommer de att glömmas bort.
Om du behöver ställa in variabler och ha dem fortfarande inställda i slutet av slingan kan du skriva om den för att använda processersättning så här:
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"
Observera att om du försöker kopiera och klistra in detta på kommandoraden , read line
förbrukar echo "$i files processed"
, så att kommandot inte körs.
För att undvika detta måste du kunde ta bort read line </dev/tty
och skicka resultatet till en personsökare som less
.
ANMÄRKNINGAR
Jag tog bort semikolonerna (;
) inuti slinga. Du kan sätta tillbaka dem om du vill, men de behövs inte.
Dessa dagar är $(command)
vanligare än `command`
. Detta beror främst på att det är lättare att skriva $(command1 $(command2))
än `command1 \`command2\``
.
read char
läser verkligen inte ett tecken.Den läser en hel rad så jag ändrade den till read line
.
Kommentarer
- sätter
while
i en pipeline kan skapa problem med den subshell som skapats (variabler i loopblocket visas inte efter att kommandot har slutförts till exempel). Med bash skulle jag använda omdirigering och processbyte:while read -r -d $'\0' file; do ...; done < <(find ... -print0)
- Visst, eller med hjälp av en heredoc:
while read; do; done <<EOF "$(find)" EOF
. Inte så lätt att läsa dock. - @glenn jackman: Jag försökte lägga till mer förklaring just nu. Gjorde jag det bara bättre eller sämre?
- Du behöver ' t behöver
IFS, -print0, while
ochread
om du hanterarfind
till fullo, som visas nedan i min lösning. - Din första lösning klarar alla tecken utom newline om du också stänger av globbing med
set -f
.
Svar
Det här skriptet misslyckas om något filnamn innehåller mellanslag eller skal med globtecken \[?*
. Kommandot find
matar ut ett filnamn per rad. Därefter utvärderas kommandosubstitutionen `find …`
av skalet enligt följande:
- Kör kommandot
find
, ta tag i dess utdata. - Dela upp
find
-utdata i separata ord. Varje mellanslagstecken är en ordseparator. - Om det är ett globmönster för varje ord, expanderar du det till listan över filer som det matchar.
Till exempel, antar att det finns tre filer i den aktuella katalogen, som heter `foo* bar.csv
, foo 1.txt
och foo 2.txt
.
- Kommandot
find
returnerar./foo* bar.csv
. - Skalet delar upp den här strängen i utrymmet och producerar två ord:
./foo*
ochbar.csv
. - Eftersom
./foo*
innehåller en globerande metatecken, den utvidgas till listan över matchande filer:./foo 1.txt
och./foo 2.txt
. - Därför körs
for
-slingan successivt med./foo 1.txt
,./foo 2.txt
ochbar.csv
.
Du kan undvika de flesta problem i detta skede genom att tona ner orddelning och vrida ing av globbing. För att nedtona uppdelningen av ord, ställ in IFS
variabeln till en enda nylinjetecken; på det här sättet delas utdata från find
endast i nya rader och mellanslag kommer att finnas kvar. Om du vill stänga av globbing kör du set -f
. Då kommer den här delen av koden att fungera så länge inget filnamn innehåller ett nytt radtecken.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Detta är inte en del av ditt problem, men jag rekommenderar att du använder $(…)
över `…`
. De har samma betydelse, men backquote-versionen har konstiga citatregler.)
Det finns ett annat problem nedan: diff $file /some/other/path/$file
ska vara
diff "$file" "/some/other/path/$file"
Annars är värdet $file
delas upp i ord och orden behandlas som globmönster, som med kommandot substitutio ovan. Om du måste komma ihåg en sak om skalprogrammering, kom ihåg detta: Använd alltid dubbla citat runt variabla utvidgningar ($foo
) och kommandosubstitutioner ( $(bar)
) , såvida du inte vet att du vill dela. (Ovan visste vi att vi ville dela upp find
i linjer.)
Ett tillförlitligt sätt att ringa find
säger att den ska köra ett kommando för varje fil som den hittar:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
I det här fallet är en annan metod att jämföra de två katalogerna, men du måste exkluderar uttryckligen alla ”tråkiga” filer.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Kommentarer
- I ' glömde bort jokertecken som en annan anledning att citera ordentligt. Tack! 🙂
- istället för
find -exec sh -c 'cmd 1; cmd 2' ";"
bör du användafind -exec cmd 1 {} ";" -exec cmd 2 {} ";"
, eftersom skalet måste maskera parametrarna, men hitta inte ' t. I det speciella fallet här behöver echo " $ 0 " inte ' t måste vara del av skriptet, lägg bara till -print efter';'
. Du inkluderade inte ' en fråga för att fortsätta, men även det kan göras genom att hitta, som visas nedan i min själ. 😉 - @userunknown: Användningen av
{}
som en delsträng av en parameter ifind -exec
är inte bärbar, att ' varför skalet behövs.Jag förstår inte ' vad du menar med ”skalet måste maskera parametrarna”; om det ' handlar om att citera, är min lösning korrekt citerad. Du ' har rätt attecho
-delen kan utföras av-print
istället.-okdir
är ett ganska nytt GNU-söktillägg, det ' är inte tillgängligt överallt. Jag inkluderade inte ' väntan på att gå vidare eftersom jag anser att extremt dåligt användargränssnitt och askaren lätt kan sättaread
i skalutdraget om han vill. - Citat är en form av maskering, är det inte '? Jag förstår inte ' din kommentar om vad som är bärbart och vad inte. Ditt exempel (2: a från botten) använder -exec för att åberopa
sh
och använder{}
– så var är mitt exempel (bredvid -okdir) mindre bärbara?find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
- ”Maskering” är inte ' t vanlig terminologi i skallitteraturen, så du ' Jag måste förklara vad du menar om du vill bli förstådd. Mitt exempel använder
{}
bara en gång och i ett separat argument; andra fall (används två gånger eller som underlag) är inte bärbara. ”Bärbar” betyder att den ' fungerar på alla unix-system; en bra riktlinje är POSIX / Single Unix-specifikationen .
Svar
Jag är förvånad över att inte se readarray
. Det gör det väldigt enkelt när det används i kombination med <<<
operator:
$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words|
Med hjälp av <<<"$expansion"
-konstruktionen kan du också dela variabler som innehåller nya rader i arrays, som :
$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset
readarray
har varit i Bash i flera år nu, så detta borde förmodligen vara det kanoniska sättet att göra detta i Bash.
Svar
Afaik hitta har allt du behöver.
find . -okdir diff {} /some/other/path/{} ";"
find tar hand om att ringa programmen på ett säkert sätt. -okdir kommer att fråga dig innan diff (är du säker på att ja / nej).
Inget skal involverat, ingen globbing, skämtare pi, pa, po.
Som sidotips: Om du kombinerar hitta med / medan / gör / xargs, i de flesta fall, y du gör det fel. 🙂
Kommentarer
- Tack för svaret. Varför gör du det fel om du kombinerar find med for / while / do / xargs?
- Hitta redan iterat över en delmängd av filer. De flesta som dyker upp med frågor kan bara använda en av åtgärderna (-ok (dir) -exec (dir), -delete) i kombination med "; " eller + (senare för parallell anrop). Den främsta anledningen till att göra det är att du inte ' inte behöver lura med filparametrar och maskerar dem för skalet. Inte så viktigt: Du behöver ' inga nya processer hela tiden, mindre minne, mer hastighet. kortare program.
- Inte här för att krossa din anda, men jämför:
time find -type f -exec cat "{}" \;
medtime find -type f -print0 | xargs -0 -I stuff cat stuff
.xargs
-versionen var snabbare med 11 sekunder när 10000 tomma filer bearbetades. Var försiktig när du hävdar att det i de flesta fall är fel att kombinerafind
med andra verktyg.-print0
och-0
finns för att hantera mellanslag i filnamnen genom att använda en nollbyte som artikelavgränsare snarare än ett mellanslag. - @JonathanKomar: Din find / exec-kommando tog 11,7 s på mitt system med 10.000 filer, xargs version 9.7 s,
time find -type f -exec cat {} +
som föreslog i min tidigare kommentar tog 0,1 s. Observera den subtila skillnaden mellan " det är fel " och " du ' gör det fel ", särskilt när det är dekorerat med en smilie. Gjorde du till exempel fel? 😉 BTW, mellanslag i filnamnet är inga problem för ovanstående kommando och hittar i allmänhet. Lastkultprogrammerare? Och förresten, det är bra att kombinera hitta med andra verktyg, bara xargs är för det mesta överflödig. - @userunknown Jag förklarade hur min kod handlar om utrymmen för eftertiden (utbildning för framtida tittare), och var antyder inte att din kod inte gör det.
+
för parallella samtal är mycket snabbt, som du nämnde. Jag skulle inte säga programmerare för lastkult, eftersom denna förmåga att användaxargs
på detta sätt kommer till nytta vid många tillfällen. Jag håller mer med Unix-filosofin: gör en sak och gör det bra (använd program separat eller i kombination för att få ett jobb gjort).find
går en fin linje där.
Svar
Släpa igenom alla filer ( alla specialtecken ingår) med helt säkert hitta (se länken för 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
- Tack för att du nämner
-d ''
. Jag insåg inte ' att$'\0'
var samma som''
vara. Bra lösning också. - Jag gillar frikopplingen av find och while, tack.
Svar
Jag är förvånad över att ingen nämnde den uppenbara zsh
-lösningen här än:
for file (**/*.csv(ND.)) { do-something-with $file }
((D)
för att även inkludera dolda filer, (N)
för att undvika felet om det inte passar, (.)
för att begränsa till vanliga filer.)
bash4.3
och ovan stöder det nu också delvis:
shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done
Svar
Filnamn med mellanslag i dem ser ut som flera namn på kommandoraden om de ” citeras inte. Om din fil heter ”Hello World.txt” expanderar diff-raden till:
diff Hello World.txt /some/other/path/Hello World.txt
som ser ut som fyra filnamn. Lägg bara citat kring argumenten:
diff "$file" "/some/other/path/$file"
Kommentarer
- Detta hjälper men det gör inte ' t lösa mitt problem. Jag ser fortfarande fall där filen delas upp i flera tokens.
- Det här svaret är vilseledande. Problemet är kommandot
for file in `find . -name "*.csv"`
. Om det finns en fil som heterHello World.csv
, kommerfile
att ställas in på./Hello
World.csv
. Citera$file
vann ' t hjälp.
Svar
Dubbel citering är din vän.
diff "$file" "/some/other/path/$file"
Annars blir variabelns innehåll orduppdelat.
Kommentarer
- Detta är vilseledande. Problemet är
for file in `find . -name "*.csv"`
-kommandot. Om det finns en fil som heterHello World.csv
,file
kommer att ställas in till./Hello
och sedan tillWorld.csv
. Citera$file
vann ' t hjälp.
Svar
Med bash4 kan du också använda den inbyggda mapfile-funktionen för att ställa in en matris som innehåller varje rad och itera på denna array.
$ 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
Mellanrummen i värdena kan undvikas med så enkelt för loopkonstruktion
for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done
ls -l root / somedir c innehåller min fil med mellanslag
Utdata ovanför min fil med mellanslag
för att undvika denna utdata, enkel lösning (se dubbla citat)
for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done
mata ut min fil med mellanslag
försökt på bash
Kommentarer
- “Looping through files ”- det är vad frågan säger. Din lösning matar ut hela
ls -l
utdata på en gång . Det är effektivt ekvivalent medecho "CHECKSTR `ls -l /root/somedir`"
.