Looping genom filer med mellanslag i namnen? [duplicera]

<åt sidan class = "s-notice s-notice__info js-post-notice mb16" role = "status">

Denna fråga har redan svar här :

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:

  1. Som standard delar skalet utdata från ett kommando i mellanslag, flikar och nya rader
  2. Filnamn kan innehålla jokertecken som skulle utvidgas
  3. 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 och read om du hanterar find 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:

  1. Kör kommandot find, ta tag i dess utdata.
  2. Dela upp find -utdata i separata ord. Varje mellanslagstecken är en ordseparator.
  3. 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.

  1. Kommandot find returnerar ./foo* bar.csv.
  2. Skalet delar upp den här strängen i utrymmet och producerar två ord: ./foo* och bar.csv.
  3. Eftersom ./foo* innehåller en globerande metatecken, den utvidgas till listan över matchande filer: ./foo 1.txt och ./foo 2.txt.
  4. Därför körs for -slingan successivt med ./foo 1.txt, ./foo 2.txt och bar.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ända find -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 i find -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 att echo -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ätta read 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 "{}" \; med time 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 kombinera find 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ända xargs 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 heter Hello World.csv, kommer file 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 heter Hello World.csv, file kommer att ställas in till ./Hello och sedan till World.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 med echo "CHECKSTR `ls -l /root/somedir`".

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *