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 IFSgö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
setsom visas i det sista exemplet är korrekt.Kodavsnittet förutsätter attset +fvar aktivt i början och återställer därför inställningen i slutet. Detta antagande kan dock vara fel. Vad händer omset -fvar 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 harlocaloch du vill stödja det avstängda fallet; om du vill återställa den, rekommenderar jag att du verkställer invarianten attIFSförblir inställd. - Med
localskulle verkligen vara den bästa lösningen, förlocal -gör skalalternativen lokala ochlocal IFSgörIFSlokal. Tyvärr ärlocalendast giltig inom funktioner, vilket gör omstrukturering av kod nödvändig. Ditt förslag att införa policyn attIFSalltid ä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 readloop 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 | whileloop 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
IFSkorrekt. Den här lösningen har också ett problem: Vad händer omIFSinte ä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 yourIFS = `(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 readkonstruera.
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
readarrayi 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
-
ProcessText1håller nya radtecken i slutet av rader -
ProcessText1bearbetar 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. -
ProcessText1betraktar 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, kommerProcessText1att 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 tillProcessLinei 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 -rechotillprintf %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/tmpsom ä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. medprintfröret.printf "%s\n" "$var" | while IFS= read -r line