Jag har en variabel som innehåller flertrinsutdata från ett kommando. Vad är det mest effektiva sättet att läsa utgången rad för rad från variabeln?
Till exempel:
jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi
Svar
Du kan använda en stundslinga med processbyte:
while read -r line do echo "$line" done < <(jobs)
Ett optimalt sätt att läs en flervärdsvariabel är att ställa in en tom IFS
variabel och printf
variabeln med en efterföljande ny rad:
# Printf "%s\n" "$var" is necessary because printf "%s" "$var" on a # variable that doesn"t end with a newline then the while loop will # completely miss the last line of the variable. while IFS= read -r line do echo "$line" done < <(printf "%s\n" "$var")
Anmärkning: Enligt shellcheck sc2031 är användning av processstationer att föredra framför ett rör för att undvika [subtilt] skapa en subshell.
Tänk också på att genom att namnge variabeln jobs
kan det orsaka förvirring eftersom det också är namnet på ett vanligt skalkommando.
Kommentarer
Svar
För att bearbeta utdata från en kommandorad för rad ( förklaring ):
jobs | while IFS= read -r line; do process "$line" done
Om du har data i en variabel redan:
printf %s "$foo" | …
printf %s "$foo"
är nästan identisk med echo "$foo"
, men skriver ut $foo
bokstavligen, medan echo "$foo"
kan tolka $foo
som ett alternativ till ekokommandot om det börjar med en -
och kan utvidga snedstreckssekvenser i $foo
i vissa skal.
Observera att i vissa skal (ask, bash, pdksh, men inte ksh eller zsh) körs den högra sidan av en rörledning i en separat process, så alla variabler som du anger i slingan går förlorade. Till exempel skriver följande radräkningsskript 0 i dessa skal:
n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n
En lösning är att lägga resten av skriptet (eller åtminstone delen som behöver värdet för $n
från slingan) i en kommandolista:
n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n }
Om att agera på de icke-tomma raderna är tillräckligt bra och ingången är inte enorm, du kan använda orduppdelning:
IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS
Förklaring: inställning IFS
till en enda ny rad gör att uppdelning av ord sker endast vid nya rader (i motsats till alla blankstegstecken under standardinställningen). set -f
stänger av globbing (dvs. jokerteckenutvidgning), vilket annars skulle hända resultatet av en kommandosubstitution $(jobs)
eller en variabel substitution $foo
. for
slingan verkar på alla delar av $(jobs)
, som alla är de icke-tomma raderna i kommandoutgången. Slutligen återställ globnings- och IFS
-inställningarna till värden som motsvarar standardvärdena.
Kommentarer
- Jag har haft problem med att ställa in IFS och avaktivera IFS. Jag tror att det rätta är att lagra det gamla värdet på IFS och ställa tillbaka IFS till det gamla värdet. Jag ’ är inte en bash-expert, men enligt min erfarenhet får du tillbaka till den ursprungliga bahfæren.
- @BjornRoche: inuti en funktion, använd
local IFS=something
. Det påverkar inte ’ det globala värdet. IIRC,unset IFS
gör ’ inte tillbaka till standardinställningen (och definitivt inte ’ t fungerar om det inte var ’ t som standard i förväg). - Jag undrar om jag använder
set
som visas i det sista exemplet är korrekt.Kodavsnittet förutsätter attset +f
var aktivt i början och återställer därför inställningen i slutet. Detta antagande kan dock vara fel. Vad händer omset -f
var aktiv i början? - @Binarus Jag återställer bara inställningar som motsvarar standardvärdena. Om du vill återställa de ursprungliga inställningarna måste du göra mer arbete. För
set -f
, spara originalet$-
. FörIFS
är det ’ irriterande fiddly om du inte ’ t harlocal
och du vill stödja det avstängda fallet; om du vill återställa den, rekommenderar jag att du verkställer invarianten attIFS
förblir inställd. - Med
local
skulle verkligen vara den bästa lösningen, förlocal -
gör skalalternativen lokala ochlocal IFS
görIFS
lokal. Tyvärr ärlocal
endast giltig inom funktioner, vilket gör omstrukturering av kod nödvändig. Ditt förslag att införa policyn attIFS
alltid är inställt låter också mycket rimligt och löser den största delen av problemet. Tack!
Svar
Problem: om du använder en loop går det i subshell och alla variabler kommer att vara förlorat. Lösning: använd för loop
# change delimiter (IFS) to new line. IFS_BAK=$IFS IFS=$"\n" for line in $variableWithSeveralLines; do echo "$line" # return IFS back if you need to split new line by spaces: IFS=$IFS_BAK IFS_BAK= lineConvertedToArraySplittedBySpaces=( $line ) echo "{lineConvertedToArraySplittedBySpaces[0]}" # return IFS back to newline for "for" loop IFS_BAK=$IFS IFS=$"\n" done # return delimiter to previous value IFS=$IFS_BAK IFS_BAK=
Kommentarer
- TACK så mycket !! Alla ovanstående lösningar misslyckades för mig.
- piping till en
while read
loop i bash betyder att while-loop är i en subshell, så variablerna är inte ’ t globalt.while read;do ;done <<< "$var"
gör att loop-kroppen inte är en subshell. (Senaste bash har ett alternativ att placera kroppen för encmd | while
loop inte i en subshell, som ksh alltid har haft.) - Se även det här relaterade inlägget .
- I liknande situationer tyckte jag att det var förvånansvärt svårt att behandla
IFS
korrekt. Den här lösningen har också ett problem: Vad händer omIFS
inte är inställt alls i början (dvs. är odefinierat)? Det kommer att definieras i alla fall efter det kodavsnittet; ’ verkar inte vara korrekt.
Svar
jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs"
Referenser:
Kommentarer
-
-r
är också en bra idé; Det förhindrar\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your
IFS = `(vilket är väsentligt för att förhindra att jag tappar utrymme) - Endast denna lösning fungerade för mig. Tack brah.
- Lider inte ’ t denna lösning av samma problem som nämns i kommentarerna till @dogbane ’ s svar? Vad händer om den sista raden i variabeln inte avslutas med en ny rad?
- Det här svaret ger det renaste sättet att mata innehållet i en variabel till
while read
konstruera.
Svar
I de senaste bash-versionerna använder du mapfile
eller readarray
för att effektivt läsa kommandoutput i matriser
$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305
Ansvarsfriskrivning: hemskt exempel, men du kan väl komma upp med ett bättre kommando att använda än ls dig själv
Kommentarer
- Det ’ ett trevligt sätt, men kullar / var / tmp med tillfälliga filer på mitt system. +1 ändå
- @eugene: det ’ är roligt. Vilket system (distro / OS) är det på?
- Det ’ s FreeBSD 8. Hur man reproducerar: sätt
readarray
i en funktion och ring funktionen några gånger. - Trevlig, @sehe. +1
Svar
De vanliga mönstren för att lösa det här problemet finns i de andra svaren.
Jag vill dock lägga till mitt tillvägagångssätt, även om jag inte är säker på hur effektiv det är. Men det är (åtminstone för mig) ganska förståeligt, ändrar inte den ursprungliga variabeln (alla lösningar som använder read
måste ha variabeln i fråga med en efterföljande ny linje och därför lägga till den, som ändrar variabeln), skapar inte subshells (som alla rörbaserade lösningar gör), använder inte här -strängar (som har sina egna problem) och använder inte processersättning (ingenting emot det, men lite svårt att förstå ibland).
Egentligen förstår jag inte varför bash
”: s integrerade RE används så sällan.Kanske är de inte bärbara, men eftersom OP har använt bash
-taggen kommer det inte att stoppa mig 🙂
#!/bin/bash function ProcessLine() { printf "%s" "$1" } function ProcessText1() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done ProcessLine "$Text" } function ProcessText2() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } function ProcessText3() { local Text=$1 local Pattern=$"^([^\n]*\n?)(.*)$" while [[ ("$Text" != "") && ("$Text" =~ $Pattern) ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } MyVar1=$"a1\nb1\nc1\n" MyVar2=$"a2\n\nb2\nc2" MyVar3=$"a3\nb3\nc3" ProcessText1 "$MyVar1" ProcessText1 "$MyVar2" ProcessText1 "$MyVar3"
Output:
root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts#
Några anteckningar:
Beteendet beror på vilken variant av ProcessText
du använder. I exemplet ovan har jag använt ProcessText1
.
Observera att
-
ProcessText1
håller nya radtecken i slutet av rader -
ProcessText1
bearbetar den sista raden i variabeln (som innehåller textenc3
) även om den raden inte innehåller ett efterföljande newline-tecken. På grund av den saknade efterföljande newline läggs kommandotolken efter skriptkörningen till den sista rad på variabeln utan att separeras från utdata. -
ProcessText1
betraktar alltid delen mellan den sista ny raden i variabeln och slutet på variabeln som en rad , även om den är tom; naturligtvis har den raden, oavsett om den är tom eller inte, inte en efterföljande nylinjetecken. Det vill säga, även om det sista tecknet i variabeln är en ny rad, kommerProcessText1
att behandla den tomma delen (nullsträng) mellan den sista nya raden och slutet på variabeln som en (ännu tom) rad och skickar den till radbearbetning. Du kan enkelt förhindra detta genom att sätta in det andra samtalet tillProcessLine
i ett lämpligt check-if-tom-tillstånd. Jag tycker dock att det är mer logiskt att lämna det som det är.
ProcessText1
måste ringa ProcessLine
på två ställen, vilket kan vara obekvämt om du vill placera ett kodblock där som direkt bearbetar linjen, istället för att anropa en funktion som behandlar linjen; du måste upprepa koden som är felbenägen.
Däremot bearbetar ProcessText3
linjen eller anropar respektive funktion bara på en plats, vilket gör att du byter ut funktionsanropet med en kod blockerar en no-brainer. Detta kostar två while
villkor istället för en. Bortsett från implementeringsskillnaderna uppför sig ProcessText3
exakt samma som ProcessText1
, förutom att den inte tar hänsyn till delen mellan den sista nylinjetecknet variabeln och slutet på variabeln som rad om den delen är tom. Det vill säga ProcessText3
kommer inte att gå in i linjebehandling efter variabelns sista newline-tecken om det newline-tecknet är det sista tecknet i variabeln.
ProcessText2
fungerar som ProcessText1
, förutom att rader måste ha en efterföljande nylinjetecken. Det vill säga, delen mellan den sista newline-karaktären i variabeln och slutet på variabeln inte anses vara en rad och kastas tyst bort. Följaktligen, om variabeln inte innehåller något nylinjetecken, sker ingen radbearbetning alls.
Jag gillar det tillvägagångssättet mer än de andra lösningarna som visas ovan, men förmodligen har jag saknat något (inte mycket erfaren av bash
programmering, och inte är mycket intresserad av andra skal).
Svar
Du kan använda < < < för att helt enkelt läsa från variabeln som innehåller den nya raden -separerade data:
while read -r line do echo "A line of input: $line" done <<<"$lines"
Kommentarer
- Välkommen till Unix & Linux! Detta duplicerar i huvudsak ett svar från fyra år sedan. Skicka inte ett svar om du inte har något nytt att bidra med.
while IFS= read
…. Om du vill förhindra \ tolkning, använd sedanread -r
echo
tillprintf %s
, så att ditt skript fungerar även med icke-taminmatning./tmp
-katalogen är skrivbar, eftersom den förlitar sig på att vara kunna skapa en tillfällig arbetsfil. Om du någonsin befinner dig i ett begränsat system med/tmp
som är skrivskyddad (och inte kan ändras av dig), kommer du att vara glad över möjligheten att använda en alternativ lösning, t.ex. g. medprintf
röret.printf "%s\n" "$var" | while IFS= read -r line