Hur kan jag läsa rad för rad från en variabel i bash?

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

  • Om du vill behålla hela ditt utrymme, använd sedan while IFS= read …. Om du vill förhindra \ tolkning, använd sedan read -r
  • Jag ’ har fixat de punkter som fred.bear nämnde samt ändrat echo till printf %s, så att ditt skript fungerar även med icke-taminmatning.
  • För att läsa från en multilinjevariabel är en herring bättre än piping från printf (se l0b0 ’ s svar).
  • @ata Även om jag ’ har hört detta ” föredraget ” tillräckligt ofta, det måste noteras att en herring alltid kräver att /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. med printf röret.
  • I det andra exemplet, om variabeln med flera rader inte ’ t efterföljande newline kommer du att förlora det sista elementet. Ändra till: printf "%s\n" "$var" | while IFS= read -r line

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 att set +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 om set -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ör IFS är det ’ irriterande fiddly om du inte ’ t har local och du vill stödja det avstängda fallet; om du vill återställa den, rekommenderar jag att du verkställer invarianten att IFS förblir inställd.
  • Med local skulle verkligen vara den bästa lösningen, för local - gör skalalternativen lokala och local IFS gör IFS lokal. Tyvärr är local endast giltig inom funktioner, vilket gör omstrukturering av kod nödvändig. Ditt förslag att införa policyn att IFS 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 en cmd | 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 om IFS 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 texten c3) ä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, kommer ProcessText1 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 till ProcessLine 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.

Lämna ett svar

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