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:
- Standaard splitst de shell de uitvoer van een commando op spaties, tabs en nieuwe regels
- Bestandsnamen kunnen jokertekens bevatten die zou worden uitgebreid
- 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
enread
als jefind
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:
- Voer de opdracht
find
uit, pak de uitvoer ervan. - Splits de
find
uitvoer in afzonderlijke woorden. Elk witruimteteken is een woordscheidingsteken. - 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
.
- Het
find
commando geeft./foo* bar.csv
terug. - De shell splitst deze string in de spatie en produceert twee woorden:
./foo*
enbar.csv
. - Sinds
./foo*
bevat een globbing metateken, het wordt uitgebreid tot de lijst met overeenkomende bestanden:./foo 1.txt
en./foo 2.txt
. - Daarom wordt de
for
-lus achtereenvolgens uitgevoerd met./foo 1.txt
,./foo 2.txt
enbar.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 jefind -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 infind -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 hetecho
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 vragenstellerread
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 "{}" \;
mettime find -type f -print0 | xargs -0 -I stuff cat stuff
. Dexargs
-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 vanfind
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 omxargs
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 naamHello World.csv
, wordtfile
ingesteld op./Hello
en vervolgens opWorld.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 naamHello World.csv
,file
wordt ingesteld op./Hello
en vervolgens opWorld.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 metecho "CHECKSTR `ls -l /root/somedir`"
.