Door bestanden met spaties in de namen doorlopen? [duplicate]

Deze vraag heeft hier al antwoorden :

Reacties

  • Ik ben het er niet mee eens dat dit een duplicaat zou zijn. Het geaccepteerde antwoord geeft aan hoe je over bestandsnamen heen kunt lopen met spaties; dat heeft niets te maken met " waarom is looping over find ' s output slechte praktijk ". Ik vond deze vraag (niet de andere) omdat ik bestandsnamen met spaties moet doorlopen, zoals in: voor bestand in $ LIST_OF_FILES; do … waar $ LIST_OF_FILES niet de uitvoer is van find; het ' is slechts een lijst met bestandsnamen (gescheiden door nieuwe regels).
  • @CarloWood – bestandsnamen kunnen nieuwe regels bevatten, dus uw vraag is vrij uniek: een lijst met bestandsnamen die spaties kunnen bevatten, maar geen nieuwe regels. Ik denk dat je ' de IFS-techniek zult moeten gebruiken om aan te geven dat de breuk optreedt bij ' \ n '
  • @ Diagonwoah, ik heb me nooit gerealiseerd dat bestandsnamen nieuwe regels mogen bevatten. Ik gebruik voornamelijk (alleen) linux / UNIX en daar zijn zelfs spaties zeldzaam; Ik heb zeker in mijn hele leven nooit nieuwe regels zien worden gebruikt: p. Dat kunnen ze net zo goed verbieden.
  • @CarloWood – bestandsnamen eindigen op een null (' \ 0 ' , hetzelfde als ' '). Al het andere is acceptabel.
  • @CarloWood Je moet onthouden dat mensen eerst stemmen en daarna lezen …

Antwoord

Kort antwoord (het dichtst bij uw antwoord, maar met spaties)

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" 

Beter antwoord (verwerkt ook jokertekens en nieuwe regels in bestandsnamen)

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 antwoord (gebaseerd op Gilles ” answer )

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

Of nog beter, om te voorkomen dat er een per bestand:

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 {} + 

Lang antwoord

Je hebt drie problemen:

  1. Standaard splitst de shell de uitvoer van een commando op spaties, tabs en nieuwe regels
  2. Bestandsnamen kunnen jokertekens bevatten die zou worden uitgebreid
  3. Wat als er een directory is waarvan de naam eindigt op *.csv?

1. Alleen splitsen op nieuwe regels

Om erachter te komen waarnaar file moet worden ingesteld, moet de shell de uitvoer aannemen van find en interpreteer het op de een of andere manier, anders zou file gewoon de volledige uitvoer zijn van find .

De shell leest de IFS variabele, die standaard is ingesteld op <space><tab><newline>.

Vervolgens kijkt het naar elk teken in de uitvoer van find. Zodra het een teken ziet dat “s in IFS, denkt het dat dit het einde van de bestandsnaam aangeeft, dus het stelt file naar welke tekens het tot nu toe zag en voert de lus uit. Dan begint het waar het was gebleven om de volgende bestandsnaam te krijgen, en voert de volgende lus uit, enz., totdat het einde van de uitvoer is bereikt.

Dus het doet dit effectief:

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

Om het te vertellen dat de invoer alleen op nieuwe regels moet worden gesplitst, moet u doen

IFS=$"\n" 

vóór uw for ... find -opdracht.

Dat stelt IFS in op een enkele nieuwe regel, dus het splitst zich alleen op nieuwe regels, en niet ook op spaties en tabs.

Als u sh of dash in plaats van ksh93, bash of zsh, moet u IFS=$"\n" zoals in plaats daarvan:

IFS=" " 

Dat is waarschijnlijk genoeg om je script werkend te krijgen, maar als je “geïnteresseerd bent om enkele andere hoekgevallen correct af te handelen, lees dan verder …

2. Uitbreiden van $file zonder wildcards

Binnen de lus waar je dat doet

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

de shell probeert $file uit te breiden (nogmaals!).

Het kan spaties bevatten, maar aangezien we IFS hierboven, dat zal hier geen probleem zijn.

Maar het kan ook jokertekens bevatten, zoals * of ?, wat tot onvoorspelbaar gedrag zou leiden. (Met dank aan Gilles om hierop te wijzen.)

Om de shell te vertellen geen jokertekens uit te breiden, plaats de variabele tussen dubbele aanhalingstekens, bijv.

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

Hetzelfde probleem zou ons ook kunnen bijten

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

Bijvoorbeeld, als je deze drie bestanden had

file1.csv file2.csv *.csv 

(zeer onwaarschijnlijk, maar nog steeds mogelijk)

Het zou zijn alsof je had gelopen

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

die wordt uitgebreid tot

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

waardoor file1.csv en file2.csv om tweemaal te worden verwerkt.

In plaats daarvan moeten we

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 leest regels uit standaardinvoer, splitst de regel in woorden volgens IFS en slaat ze op in de variabelenamen die u specificeert.

Hier vertellen we het niet om de regel in woorden te splitsen, en om de regel op te slaan in $file.

Merk ook op dat is gewijzigd in read line </dev/tty.

Dit komt omdat binnen de lus de standaardinvoer afkomstig is van find via de pijplijn.

Als we net read deden, zou het een deel of de hele bestandsnaam in beslag nemen en zouden sommige bestanden worden overgeslagen .

/dev/tty is de terminal waar de gebruiker het script vanaf draait. Merk op dat dit een fout zal veroorzaken als het script wordt uitgevoerd via cron, maar ik neem aan dat dit in dit geval niet belangrijk is.

Wat als een bestandsnaam nieuwe regels bevat?

We kunnen dat oplossen door -print te veranderen in -print0 en read -d "" aan het einde van een 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 

Dit zorgt ervoor dat find een nulbyte aan het einde van elke bestandsnaam plaatst. Null-bytes zijn de enige tekens die niet zijn toegestaan in bestandsnamen, dus dit zou alle mogelijke bestandsnamen moeten behandelen, hoe raar ook.

Om de bestandsnaam aan de andere kant te krijgen, gebruiken we IFS= read -r -d "".

Waar we hierboven read gebruikten, gebruikten we het standaard scheidingsteken voor nieuwe regels, maar nu find gebruikt null als het lijnscheidingsteken. In bash kun je “geen NUL-teken in een argument doorgeven aan een commando (zelfs ingebouwde degenen), maar bash begrijpt -d "" in de betekenis van NUL gescheiden . Daarom gebruiken we -d "" om read gebruik hetzelfde lijnscheidingsteken als find. Merk op dat -d $"\0" overigens ook werkt, omdat bash die NUL-bytes niet ondersteunt, behandelt het als de lege string.

Om correct te zijn, voegen we ook -r toe, wat zegt dat je geen backslashes moet verwerken in bestandsnamen speciaal. Zonder -r, worden \<newline> verwijderd en wordt \n omgezet in n.

Een meer draagbare manier om dit te schrijven waarvoor geen bash of of het onthouden van alle bovenstaande regels over null bytes (nogmaals, met dank aan Gilles):

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

* 3. Mappen overslaan waarvan namen eindigen op .csv

find . -name "*.csv" 

komen ook overeen met mappen die something.csv.

Om dit te voorkomen, voegt u -type f toe aan het find commando.

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

Zoals glenn jackman aangeeft, zijn in beide voorbeelden de uit te voeren opdrachten voor elk bestand wordt uitgevoerd in een subshell, dus als u variabelen binnen de lus wijzigt, worden ze vergeten.

Als u variabelen moet instellen en ze nog steeds moet instellen aan het einde van de lus kun je het herschrijven om procesvervanging als volgt te gebruiken:

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 op dat als je dit probeert te kopiëren en te plakken op de opdrachtregel , read line zal de echo "$i files processed" gebruiken, zodat die opdracht “niet wordt uitgevoerd.

Om dit te voorkomen, zou read line </dev/tty kunnen verwijderen en het resultaat naar een pager zoals less kunnen sturen.


OPMERKINGEN

Ik heb de puntkommas (;) verwijderd in de lus. Je kunt ze terugplaatsen als je wilt, maar ze zijn niet nodig.

Tegenwoordig komt $(command) vaker voor dan `command`. Dit komt voornamelijk doordat het “gemakkelijker is om $(command1 $(command2)) te schrijven dan `command1 \`command2\``.

read char leest een karakter niet echt.Het leest een hele regel, dus ik veranderde het in read line.

Reacties

  • zetten while in een pijplijn kunnen problemen veroorzaken met de aangemaakte subshell (variabelen in het lusblok zijn bijvoorbeeld niet zichtbaar nadat de opdracht is voltooid). Met bash zou ik invoeromleiding en procesvervanging gebruiken: while read -r -d $'\0' file; do ...; done < <(find ... -print0)
  • Zeker, of met een heredoc: while read; do; done <<EOF "$(find)" EOF . Niet zo gemakkelijk te lezen.
  • @glenn jackman: Ik heb zojuist geprobeerd meer uitleg toe te voegen. Heb ik het net beter of slechter gemaakt?
  • Je hebt ' geen IFS, -print0, while en read als je find volledig afhandelt, zoals hieronder getoond in mijn oplossing.
  • Je eerste oplossing is geschikt voor elk teken behalve newline als je globbing ook uitschakelt met set -f.

Answer

Dit script mislukt als een bestandsnaam spaties of globbing-tekens van de shell bevat \[?*. Het find commando geeft één bestandsnaam per regel weer. Vervolgens wordt de opdrachtvervanging `find …` door de shell als volgt geëvalueerd:

  1. Voer de opdracht find uit, pak de uitvoer ervan.
  2. Splits de find uitvoer in afzonderlijke woorden. Elk witruimteteken is een woordscheidingsteken.
  3. Voor elk woord, als het een globbing-patroon is, vouwt u het uit naar de lijst met bestanden waarmee het overeenkomt.

Bijvoorbeeld, stel dat de huidige directory drie bestanden bevat, genaamd `foo* bar.csv, foo 1.txt en foo 2.txt.

  1. Het find commando geeft ./foo* bar.csv terug.
  2. De shell splitst deze string in de spatie en produceert twee woorden: ./foo* en bar.csv.
  3. Sinds ./foo* bevat een globbing metateken, het wordt uitgebreid tot de lijst met overeenkomende bestanden: ./foo 1.txt en ./foo 2.txt.
  4. Daarom wordt de for -lus achtereenvolgens uitgevoerd met ./foo 1.txt, ./foo 2.txt en bar.csv.

U kunt de meeste problemen in dit stadium voorkomen door het splitsen van woorden te verminderen en ing van globbing. Om woordsplitsing af te zwakken, stelt u de variabele IFS in op één teken voor een nieuwe regel; op deze manier wordt de uitvoer van find alleen op nieuwe regels gesplitst en blijven er spaties over. Om globbing uit te schakelen, voert u set -f uit. Dan werkt dit deel van de code zolang er geen bestandsnaam een newline-teken bevat.

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

(Dit is geen onderdeel van uw probleem, maar ik raad aan om $(…) te gebruiken in plaats van `…`. Ze hebben dezelfde betekenis, maar de versie met achteraanhaling heeft rare aanhalingsregels.)

Er is een ander probleem hieronder: diff $file /some/other/path/$file zou moeten zijn

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

Anders wordt de waarde van $file wordt opgesplitst in woorden en de woorden worden behandeld als glob-patronen, zoals bij het commando substitutio hierboven. Als je één ding over shell-programmering moet onthouden, onthoud dan dit: gebruik altijd dubbele aanhalingstekens rond variabele uitbreidingen ($foo) en opdrachtvervangingen ( $(bar)) , tenzij je weet dat je wilt splitsen. (Hierboven wisten we dat we de find uitvoer in regels wilden splitsen.)

Een betrouwbare manier om find vertelt het om een commando uit te voeren voor elk bestand dat het vindt:

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

In dit geval is een andere benadering om de twee mappen te vergelijken, hoewel je wel sluit expliciet alle “saaie” bestanden uit.

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

Reacties

  • I ' d was jokertekens vergeten als een andere reden om correct te citeren. Bedankt! 🙂
  • in plaats van find -exec sh -c 'cmd 1; cmd 2' ";", zou je find -exec cmd 1 {} ";" -exec cmd 2 {} ";" moeten gebruiken, omdat de shell de parameters moet maskeren, maar vind niet ' t. In het speciale geval hier, echo " $ 0 " hoeft ' t een deel van het script, voeg gewoon -print toe na de ';'. Je hebt geen ' vraag opgenomen om verder te gaan, maar zelfs dat kan worden gedaan door te vinden, zoals hieronder in mijn soulution wordt getoond. 😉
  • @userunknown: het gebruik van {} als substring van een parameter in find -exec is niet draagbaar, dat ' is waarom de shell nodig is.Ik begrijp niet ' wat je bedoelt met “de shell moet de parameters maskeren”; als het ' over citeren gaat, wordt mijn oplossing correct geciteerd. Je ' hebt gelijk dat het echo deel in plaats daarvan zou kunnen worden uitgevoerd door -print. -okdir is een redelijk recente GNU-extensie voor zoeken, ' is niet overal beschikbaar. Ik heb ' het wachten om verder te gaan niet meegerekend omdat ik van mening ben dat de gebruikersinterface extreem slecht is en de vragensteller read gemakkelijk in het shell-fragment kan plaatsen als hij wil.
  • Citeren is een vorm van maskeren, niet ' niet? Ik ' begrijp uw opmerking over wat draagbaar is en wat niet. Uw voorbeeld (2e van beneden) gebruikt -exec om sh aan te roepen en gebruikt {} – dus waar is mijn voorbeeld (naast -okdir) minder draagbaar? find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
  • “Maskeren” is niet ' t algemene terminologie in shell-literatuur, dus u ' Ik zal moeten uitleggen wat je bedoelt als je begrepen wilt worden. In mijn voorbeeld wordt {} slechts één keer gebruikt en in een afzonderlijk argument; andere gevallen (tweemaal of als substring gebruikt) zijn niet draagbaar. “Portable” betekent dat het ' zal werken op alle Unix-systemen; een goede richtlijn is de POSIX / Single Unix-specificatie .

Answer

Het verbaast me “dat readarray niet wordt genoemd. Het maakt dit erg gemakkelijk in combinatie met de <<< operator:

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

Door de <<<"$expansion" -constructie te gebruiken, kunt u ook variabelen met nieuwe regels splitsen in arrays, zoals :

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

readarray staat nu al jaren in Bash, dus dit zou waarschijnlijk de canonieke manier moeten zijn om te doen dit in Bash.

Answer

Afaik find heeft alles wat je nodig hebt.

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

find zorgt ervoor dat de programmas veilig worden aangeroepen. -okdir zal u vragen vóór de diff (weet u zeker ja / nee).

Geen shell betrokken, geen globbing, jokers, pi, pa, po.

Als een kanttekening: als je find combineert met for / while / do / xargs, in de meeste gevallen, y je doet het verkeerd. 🙂

Reacties

  • Bedankt voor het antwoord. Waarom doe je het verkeerd als je find combineert met for / while / do / xargs?
  • Zoeken herhaalt zich al over een subset van bestanden. De meeste mensen die met vragen komen, kunnen een van de acties (-ok (dir) -exec (dir), -delete) gebruiken in combinatie met "; " of + (later voor parallelle aanroep). De belangrijkste reden om dit te doen is dat je ' t niet hoeft te rommelen met bestandsparameters en ze maskeert voor de shell. Niet zo belangrijk: je hebt ' niet steeds nieuwe processen nodig, minder geheugen, meer snelheid. korter programma.
  • Niet hier om je geest te verpletteren, maar vergelijk: time find -type f -exec cat "{}" \; met time find -type f -print0 | xargs -0 -I stuff cat stuff. De xargs -versie was 11 seconden sneller bij het verwerken van 10.000 lege bestanden. Wees voorzichtig wanneer u beweert dat in de meeste gevallen het combineren van find met andere hulpprogrammas verkeerd is. -print0 en -0 zijn er om met spaties in de bestandsnamen om te gaan door een nul-byte als itemscheidingsteken te gebruiken in plaats van een spatie.
  • @JonathanKomar: Je find / exec-commando duurde 11,7 seconden op mijn systeem met 10.000 bestanden, de xargs-versie 9.7 s, time find -type f -exec cat {} + zoals gesuggereerd in mijn vorige opmerking kostte 0.1 s. Let op het subtiele verschil tussen " het is verkeerd " en " jij ' doen het verkeerd ", vooral wanneer versierd met een smilie. Heb je het bijvoorbeeld verkeerd gedaan? 😉 Tussen haakjes, spaties in de bestandsnaam zijn geen probleem voor het bovenstaande commando en vinden in het algemeen. Cargo cult programmeur? En trouwens, het combineren van find met andere tools is prima, alleen xargs is meestal overbodig.
  • @userunknown Ik legde uit hoe mijn code omgaat met spaties voor het nageslacht (opleiding van toekomstige kijkers), en was niet implicerend dat uw code dat niet doet. De + voor parallelle oproepen is erg snel, zoals je al zei. Ik zou niet zeggen cargo cult programmeur, omdat deze mogelijkheid om xargs op deze manier te gebruiken bij talloze gelegenheden van pas komt. Ik ben het meer eens met de Unix-filosofie: doe één ding en doe het goed (gebruik programmas afzonderlijk of in combinatie om een klus te klaren). find loopt daar op een dunne lijn.

Antwoord

Loop door alle bestanden ( elk speciaal teken inbegrepen) met de volledig veilige vondst (zie de link voor documentatie):

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 

Reacties

  • Bedankt voor het vermelden van -d ''. Ik had ' niet door dat $'\0' hetzelfde was als '', maar het lijkt erop dat worden. Ook een goede oplossing.
  • Ik hou van de ontkoppeling van find en while, bedankt.

Answer

Het verbaast me dat nog niemand de voor de hand liggende zsh oplossing hier noemde:

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

((D) om ook verborgen bestanden op te nemen, (N) om de fout te vermijden als er geen overeenkomst is, (.) om te beperken tot gewone bestanden.)

bash4.3 en hoger ondersteunt het nu ook gedeeltelijk:

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

Antwoord

Bestandsnamen met spaties erin zien eruit als meerdere namen op de opdrachtregel als ze ” worden niet geciteerd. Als uw bestand de naam “Hello World.txt” heeft, wordt de diff-regel uitgebreid naar:

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

wat eruitziet als vier bestandsnamen. aanhalingstekens rond de argumenten:

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

Reacties

  • Dit helpt, maar het doet het niet ' lost mijn probleem niet op. Ik zie nog steeds gevallen waarin het bestand wordt opgesplitst in meerdere tokens.
  • Dit antwoord is misleidend. Het probleem is het for file in `find . -name "*.csv"` commando. Als er een bestand is met de naam Hello World.csv, wordt file ingesteld op ./Hello en vervolgens op World.csv. Het citeren van $file won ' t hulp.

Antwoord

Dubbel citeren is je vriend.

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

Anders wordt de inhoud van de variabele woordsplitsing.

Reacties

  • Dit is misleidend. Het probleem is het for file in `find . -name "*.csv"` commando. Als er een bestand is met de naam Hello World.csv, file wordt ingesteld op ./Hello en vervolgens op World.csv. Door $file te citeren, werd ' niet geholpen.

Antwoord

Met bash4 kun je ook de ingebouwde mapfile-functie gebruiken om een array in te stellen die elke regels bevat en deze array herhalen.

$ 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 

Answer

De spaties in de waarden kunnen worden vermeden door een simpele lusconstructie

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

ls -l root / somedir c bevat mijn bestand met spaties

Uitvoer van mijn bestand hierboven met spaties

om deze uitvoer te vermijden, eenvoudige oplossing (let op de dubbele aanhalingstekens)

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

voer mijn bestand uit met spaties

geprobeerd op bash

Reacties

  • “Doorloop bestanden ”- dat is wat de vraag zegt. Uw oplossing zal de volledige ls -l uitvoer tegelijk uitvoeren. Het is effectief equivalent met echo "CHECKSTR `ls -l /root/somedir`".

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *