Hvordan kan jeg lese linje for linje fra en variabel i bash?

Jeg har en variabel som inneholder flerlinjes utdata fra en kommando. Hva er den mest effektive måten å lese utgangen linje for linje fra variabelen?

For eksempel:

jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi 

Svar

Du kan bruke en stundsløyfe med prosessubstitusjon:

while read -r line do echo "$line" done < <(jobs) 

En optimal måte å les en flerlinjevariabel er å sette en tom IFS variabel og printf variabelen med en etterfølgende ny linje:

# 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") 

Merk: I henhold til shellcheck sc2031 , er bruk av prosessstasjon å foretrekke fremfor et rør for å unngå [subtilt] opprette en subshell.

Vær også oppmerksom på at ved å navngi variabelen jobs kan det føre til forvirring, siden det også er navnet på en vanlig skallkommando.

Kommentarer

  • Hvis du vil beholde hele det hvite området ditt, så bruk while IFS= read …. Hvis du vil forhindre \ tolkning, bruk deretter read -r
  • Jeg ‘ har løst punktene fred.bear nevnt, samt endret echo til printf %s, slik at skriptet ditt vil fungere selv med ikke-tamme inngang.
  • For å lese fra en flerlinjevariabel, er en herring å foretrekke fremfor piping fra printf (se l0b0 ‘ s svar).
  • @ata Selv om jeg ‘ har hørt dette » foretrukket » ofte nok, det må bemerkes at en herring alltid krever at /tmp katalogen er skrivbar, da den er avhengig av å være i stand til å opprette en midlertidig arbeidsfil. Hvis du noen gang befinner deg i et begrenset system med /tmp som er skrivebeskyttet (og ikke kan endres av deg), vil du være glad for muligheten for å bruke en alternativ løsning, f.eks. g. med printf -røret.
  • I det andre eksemplet, hvis flerlinjevariabelen ikke inneholder ‘ t etterfølgende newline vil du miste det siste elementet. Endre den til: printf "%s\n" "$var" | while IFS= read -r line

Svar

For å behandle utgang fra en kommandolinje for linje ( forklaring ):

jobs | while IFS= read -r line; do process "$line" done 

Hvis du har data i en variabel allerede:

printf %s "$foo" | … 

printf %s "$foo" er nesten identisk med echo "$foo", men skriver ut $foo bokstavelig, mens echo "$foo" kan tolke $foo som et alternativ til ekkokommandoen hvis den begynner med en -, og kan utvide tilbakeslagssekvenser i $foo i noen skall.

Merk at i noen skall (aske, bash, pdksh, men ikke ksh eller zsh), kjører høyre side av en rørledning i en egen prosess, så enhver variabel du setter i løkken går tapt. Følgende linjetellingskript skriver for eksempel ut 0 i disse skallene:

n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n 

En løsning er å sette resten av skriptet (eller i det minste delen som trenger verdien av $n fra sløyfen) i en kommandoliste:

n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n } 

Hvis å handle på de ikke-tomme linjene er god nok og inngangen er ikke stor, du kan bruke orddeling:

IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS 

Forklaring: innstilling IFS til en enkelt ny linje gjør at orddelingen bare forekommer ved nye linjer (i motsetning til hvilket som helst hvitt mellomromstegn under standardinnstillingen). set -f slår av globbing (dvs. utvidelse av jokertegn), som ellers vil skje med resultatet av en kommandosubstitusjon $(jobs) eller en variabel substitusjon $foo. for sløyfen virker på alle delene av $(jobs), som er alle de ikke-tomme linjene i kommandoutgangen. Til slutt gjenoppretter du globbing- og IFS -innstillingene til verdier som tilsvarer standardene.

Kommentarer

  • Jeg har hatt problemer med å sette IFS og deaktivere IFS. Jeg tror det rette å gjøre er å lagre den gamle verdien av IFS og sette IFS tilbake til den gamle verdien. Jeg ‘ er ikke en bash-ekspert, men etter min erfaring får du deg tilbake til den opprinnelige bahavioren.
  • @BjornRoche: inne i en funksjon, bruk local IFS=something. Det vil ikke ‘ ikke påvirke verdien for det globale omfanget. IIRC, unset IFS kommer ikke ‘ til å komme deg tilbake til standardverdien (og absolutt ikke ‘ t fungerer hvis det ikke var ‘ t standard på forhånd).
  • Jeg lurer på om jeg bruker set i veien vist i det siste eksemplet er riktig.Kodebiten forutsetter at set +f var aktiv i begynnelsen, og gjenoppretter derfor innstillingen til slutt. Imidlertid kan denne antagelsen være feil. Hva om set -f var aktiv i begynnelsen?
  • @Binarus Jeg gjenoppretter bare innstillinger som tilsvarer standardinnstillingene. Hvis du vil gjenopprette de opprinnelige innstillingene, må du gjøre mer arbeid. For set -f, lagre originalen $-. For IFS er det ‘ irriterende fiddly hvis du ikke ‘ ikke har local og du vil støtte den usettede saken; Hvis du vil gjenopprette det, anbefaler jeg at du håndhever invarianten for at IFS forblir satt.
  • Bruk av local ville faktisk være den beste løsningen, fordi local - gjør skallalternativene lokale, og local IFS gjør IFS lokal. Dessverre er local bare gyldig i funksjoner, noe som gjør kodestrukturering nødvendig. Forslaget ditt om å innføre policyen om at IFS alltid er satt, høres også veldig rimelig ut og løser den største delen av problemet. Takk!

Svar

Problem: Hvis du bruker mens loop, vil den kjøre i subshell og alle variablene vil være tapt. Løsning: bruk for 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

  • TUSEN TAKK !! Alle de ovennevnte løsningene mislyktes for meg.
  • rørledning til en while read sløyfe i bash betyr at mens sløyfen er i en subshell, så variabler er ikke ‘ t global. while read;do ;done <<< "$var" gjør ikke loop-kroppen til en subshell. (Nylig bash har et alternativ å sette kroppen til en cmd | while sløyfe ikke i en subshell, slik ksh alltid har hatt.)
  • Se også dette relaterte innlegget .
  • I lignende situasjoner syntes jeg det var overraskende vanskelig å behandle IFS riktig. Denne løsningen har også et problem: Hva om IFS ikke er satt i det hele tatt i begynnelsen (dvs. er udefinert)? Det vil bli definert i hvert tilfelle etter det kodebiten; dette ser ikke ut til ‘ å være riktig.

Svar

jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs" 

Referanser:

Kommentarer

  • -r er også en god ide; Det forhindrer \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(som er viktig for å forhindre at du mister tomrom)
  • Bare denne løsningen fungerte for meg. Takk brah.
  • Lider ikke ‘ t denne løsningen under det samme problemet som er nevnt i kommentarene til @dogbane ‘ s svar? Hva om den siste linjen i variabelen ikke blir avsluttet med et nytt linjetegn?
  • Dette svaret gir den reneste måten å mate innholdet i en variabel til while read konstruere.

Svar

I nyere bash-versjoner, bruk mapfile eller readarray for effektivt å lese kommandoutput i matriser

$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305 

Ansvarsfraskrivelse: fryktelig eksempel, men du kan gjerne komme opp med en bedre kommando å bruke enn ls deg selv

Kommentarer

  • Det ‘ en fin måte, men kull / var / tmp med midlertidige filer på systemet mitt. +1 uansett
  • @eugene: det ‘ er morsomt. Hvilket system (distro / OS) er det på?
  • Det ‘ s FreeBSD 8. Hvordan reprodusere: sett readarray i en funksjon og ring funksjonen et par ganger.
  • Fin, @sehe. +1

Svar

De vanlige mønstrene for å løse dette problemet er gitt i de andre svarene.

Jeg vil imidlertid legge til en tilnærming, selv om jeg ikke er sikker på hvor effektiv den er. Men den er (i det minste for meg) ganske forståelig, endrer ikke den opprinnelige variabelen (alle løsninger som bruker read må ha den aktuelle variabelen med en etterfølgende ny linje og derfor legge den til, som endrer variabelen), lager ikke subshells (som alle rørbaserte løsninger gjør), bruker ikke her -strenger (som har sine egne problemer), og bruker ikke prosesserstatning (ingenting imot det, men litt vanskelig å forstå noen ganger).

Egentlig skjønner jeg ikke hvorfor bash» s integrerte RE brukes så sjelden.Kanskje de ikke er bærbare, men siden OP har brukt bash -taggen, vil det ikke stoppe meg 🙂

#!/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# 

Et par notater:

Atferden avhenger av hvilken variant av ProcessText du bruker. I eksemplet ovenfor har jeg brukt ProcessText1.

Legg merke til at

  • ProcessText1 holder nye linjetegn på slutten av linjene
  • ProcessText1 behandler den siste linjen i variabelen (som inneholder teksten c3) selv om den linjen ikke inneholder et etterfølgende nytt linjetegn. På grunn av den manglende etterfølgende linjen, blir ledeteksten etter kjøring av skript lagt til den siste linje av variabelen uten å være skilt fra utdataene.
  • ProcessText1 anser alltid delen mellom den siste nye linjen i variabelen og slutten av variabelen som en linje , selv om den er tom; Selvfølgelig har den linjen, enten den er tom eller ikke, ikke en etterfølgende karakter. Selv om det siste tegnet i variabelen er en ny linje, vil ProcessText1 behandle den tomme delen (nullstreng) mellom den siste nye linjen og slutten av variabelen som en (ennå tom) linje og vil overføre den til linjebehandling. Du kan enkelt forhindre denne oppførselen ved å pakke den andre samtalen til ProcessLine inn i en passende kontroll-hvis-tom tilstand; Jeg synes imidlertid det er mer logisk å la det være som det er.

ProcessText1 må ringe ProcessLine på to steder, noe som kan være ubehagelig hvis du ønsker å plassere en blokk med kode der som direkte behandler linjen, i stedet for å ringe en funksjon som behandler linjen; du må gjenta koden som er feilutsatt.

Derimot behandler ProcessText3 linjen eller kaller den respektive funksjonen bare ett sted, og erstatter funksjonsanropet med en kode blokkerer en no-brainer. Dette koster to while forhold i stedet for en. Bortsett fra implementeringsforskjellene, oppfører ProcessText3 seg nøyaktig det samme som ProcessText1, bortsett fra at den ikke vurderer delen mellom den siste nye linjetegn variabelen og slutten av variabelen som linje hvis den delen er tom. Det vil si at ProcessText3 ikke vil gå inn i linjebehandling etter den siste nye linjetegnet i variabelen hvis det nye linjetegnet er det siste tegnet i variabelen.

ProcessText2 fungerer som ProcessText1, bortsett fra at linjene må ha et etterfølgende karakteristikk. Det vil si at delen mellom den siste nye linjetegnet i variabelen og slutten av variabelen ikke anses å være en linje og kastes stille. Følgelig, hvis variabelen ikke inneholder noe nylinjetegn, skjer det ingen linjebehandling i det hele tatt.

Jeg liker den tilnærmingen mer enn de andre løsningene vist ovenfor, men sannsynligvis har jeg savnet noe (ikke er veldig erfaren av bash programmering, og er ikke veldig interessert i andre skall).

Svar

Du kan bruke < < < for å bare lese fra variabelen som inneholder den nye linjen -adskilte data:

while read -r line do echo "A line of input: $line" done <<<"$lines" 

Kommentarer

  • Velkommen til Unix & Linux! Dette dupliserer i hovedsak et svar fra fire år siden. Ikke legg inn svar med mindre du har noe nytt å bidra med.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *