Hvordan kan jeg læse linje for linje fra en variabel i bash?

Jeg har en variabel, der indeholder multilinjeoutput af en kommando. Hvad er den mest effektive måde at læse output linje på linje fra variablen?

For eksempel:

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

Svar

Du kan bruge en while-loop med proceserstatning:

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

En optimal måde at læse en flerlinjevariabel er at indstille en tom IFS variabel og printf variablen ind med en efterfø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") 

Bemærk: I henhold til shellcheck sc2031 foretrækkes brugen af processtationer frem for et rør for at undgå [subtilt] oprettelse af en subshell.

Vær også opmærksom på, at ved at navngive variablen jobs kan det forårsage forvirring, da det også er navnet på en fælles shell-kommando.

Kommentarer

  • Hvis du vil beholde hele dit hvide område, skal du bruge while IFS= read …. Hvis du ønsker at forhindre \ fortolkning, brug derefter read -r
  • Jeg ‘ har rettet de punkter, fred.bear har nævnt, samt ændret echo til printf %s, så dit script fungerer selv med ikke-tamme input.
  • For at læse fra en variabel med flere linjer foretrækkes en herring frem for piping fra printf (se l0b0 ‘ s svar).
  • @ata Skønt jeg ‘ har hørt dette ” foretrukket ” ofte nok, skal det bemærkes, at en herring altid kræver, at /tmp -mappen er skrivbar, da den er afhængig af at være i stand til at oprette en midlertidig arbejdsfil. Hvis du nogensinde befinder dig i et begrænset system med /tmp, der er skrivebeskyttet (og ikke kan ændres af dig), vil du være glad for muligheden for at bruge en alternativ løsning, f.eks. g. med printf -røret.
  • I det andet eksempel, hvis variablen med flere linier ikke indeholder ‘ t efterfølgende linje mister du det sidste element. Skift det til: printf "%s\n" "$var" | while IFS= read -r line

Svar

For at behandle output af 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 næsten identisk med echo "$foo", men udskriver $foo bogstaveligt, mens echo "$foo" muligvis fortolker $foo som en mulighed til ekkokommandoen, hvis den begynder med en - og muligvis udvider tilbageslagssekvenser i $foo i nogle skaller.

Bemærk, at i nogle skaller (aske, bash, pdksh, men ikke ksh eller zsh), kører den højre side af en rørledning i en separat proces, så enhver variabel, du angiver i sløjfen, går tabt. For eksempel udskriver følgende linjetællingsscript 0 i disse skaller:

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

En løsning er at placere resten af scriptet (eller i det mindste delen der har brug for værdien af $n fra sløjfen) i en kommandoliste:

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

Hvis at handle på de ikke-tomme linjer er god nok, og input er ikke stort, du kan bruge ordopdeling:

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

Forklaring: indstilling IFS til en enkelt ny linje får kun opdeling af ord til at forekomme ved nye linjer (i modsætning til ethvert mellemrumstegn under standardindstillingen). set -f slukker globbing (dvs. udvidelse af jokertegn), hvilket ellers ville ske med resultatet af en kommandosubstitution $(jobs) eller en variabel substitution $foo. for -sløjfen virker på alle stykkerne af $(jobs), som alle er de ikke-tomme linjer i kommandooutputtet. Til sidst skal du gendanne globbing- og IFS -indstillingerne til værdier, der svarer til standardindstillingerne.

Kommentarer

  • Jeg har haft problemer med at indstille IFS og frakoble IFS. Jeg tror, at den rigtige ting at gøre er at gemme den gamle værdi af IFS og sætte IFS tilbage til den gamle værdi. Jeg ‘ Jeg er ikke en bash-ekspert, men efter min erfaring får du dig tilbage til den oprindelige bahavior.
  • @BjornRoche: inde i en funktion skal du bruge local IFS=something. Det påvirker ikke ‘ den globale værdi. IIRC, unset IFS betyder ikke, at ‘ ikke vender tilbage til standardværdien (og bestemt ikke ‘ t arbejde, hvis det ikke var ‘ t som standard på forhånd).
  • Jeg spekulerer på, om jeg bruger set i vejen vist i det sidste eksempel er korrekt.Kodestykket antager, at set +f var aktiv i starten, og gendanner derfor denne indstilling i slutningen. Denne antagelse kan dog være forkert. Hvad hvis set -f var aktiv i starten?
  • @Binarus Jeg gendanner kun indstillinger svarende til standardindstillingerne. Faktisk, hvis du vil gendanne de originale indstillinger, skal du udføre mere arbejde. For set -f, gem originalen $-. For IFS er det ‘ irriterende, hvis du ikke ‘ t har local og du vil understøtte den usættede sag; hvis du vil gendanne det, anbefaler jeg at håndhæve den uændrede, at IFS forbliver indstillet.
  • Brug af local ville faktisk være den bedste løsning, fordi local - gør shell-mulighederne lokale, og local IFS gør IFS lokal. Desværre er local kun gyldig inden for funktioner, hvilket gør kodestrukturering nødvendig. Dit forslag om at indføre politikken om, at IFS altid er indstillet, lyder også meget rimeligt og løser den største del af problemet. Tak!

Svar

Problem: Hvis du bruger mens loop, kører det i subshell, og alle variabler bliver faret vild. Løsning: brug til sløjfe

# 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

  • TAK MEGET !! Alle ovenstående løsninger mislykkedes for mig.
  • rørledning til en while read loop i bash betyder, at while-loop er i en subshell, så variabler er ‘ t global. while read;do ;done <<< "$var" gør sløjfekroppen ikke til en subshell. (Nylig bash har en mulighed for at placere kroppen af en cmd | while loop ikke i en subshell, som ksh altid har haft.)
  • Se også dette relaterede indlæg .
  • I lignende situationer fandt jeg det overraskende svært at behandle IFS korrekt. Denne løsning har også et problem: Hvad hvis IFS slet ikke er indstillet i starten (dvs. er udefineret)? Det vil blive defineret i hvert tilfælde efter dette kodestykke; dette ser ikke ud til ‘.

Svar

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

Referencer:

Kommentarer

  • -r er også en god idé; Det forhindrer \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(hvilket er vigtigt for at forhindre tab af hvidt mellemrum)
  • Kun denne løsning fungerede for mig. Tak brah.
  • Lider ikke ‘ t denne løsning under det samme problem, som er nævnt i kommentarerne til @dogbane ‘ s svar? Hvad hvis den sidste linje i variablen ikke afsluttes med et nyt linjetegn?
  • Dette svar giver den reneste måde at føje indholdet af en variabel til while read konstruere.

Svar

I nyere bash-versioner skal du bruge mapfile eller readarray for effektivt at læse kommandooutput i arrays

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

Ansvarsfraskrivelse: forfærdeligt eksempel, men du kan hurtigt komme op med en bedre kommando at bruge end ls selv

Kommentarer

  • Det ‘ på en pæn måde, men kuld / var / tmp med midlertidige filer på mit system. +1 alligevel
  • @eugene: det ‘ er sjovt. Hvilket system (distro / OS) er det på?
  • Det ‘ s FreeBSD 8. Sådan reproduceres: sæt readarray i en funktion, og ring til funktionen et par gange.
  • Dejlig, @sehe. +1

Svar

De almindelige mønstre til løsning af dette problem er angivet i de andre svar.

Jeg vil dog gerne tilføje min tilgang, selvom jeg ikke er sikker på, hvor effektiv den er. Men den er (i det mindste for mig) ganske forståelig, ændrer ikke den oprindelige variabel (alle løsninger, der bruger read skal have den pågældende variabel med en efterfølgende ny linje og derfor tilføje den, som ændrer variablen), opretter ikke subshells (som alle rørbaserede løsninger gør), bruger ikke her -strenge (som har deres egne problemer) og bruger ikke proceserstatning (intet imod det, men lidt svært at forstå nogle gange).

Jeg forstår faktisk ikke hvorfor bash” s integrerede REer bruges så sjældent.Måske er de ikke bærbare, men da OP har brugt bash tag, vil det ikke stoppe 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# 

Et par noter:

Adfærden afhænger af hvilken variant af ProcessText du bruger. I eksemplet ovenfor har jeg brugt ProcessText1.

Bemærk at

  • ProcessText1 holder nye linjetegn i slutningen af linjer
  • ProcessText1 behandler den sidste linje i variablen (som indeholder teksten c3) skønt linjen ikke indeholder et efterfølgende newline-tegn. På grund af den manglende efterfølgende newline tilføjes kommandoprompten efter scriptets udførelse til den sidste linje af variablen uden at være adskilt fra output.
  • ProcessText1 betragter altid delen mellem den sidste nye linje i variablen og slutningen af variablen som en linje , selvom den er tom; selvfølgelig har denne linje, uanset om den er tom eller ej, ikke en efterfølgende nyline-karakter. Det vil sige, at selvom det sidste tegn i variablen er en ny linje, behandler ProcessText1 den tomme del (nullstreng) mellem den sidste nye linje og slutningen af variablen som en (endnu tom) linje og sender den til liniebehandling. Du kan let forhindre denne opførsel ved at indpakke det andet opkald til ProcessLine i en passende check-if-tom-tilstand; Jeg synes dog, det er mere logisk at lade det være som det er.

ProcessText1 skal ringe til ProcessLine to steder, hvilket kan være ubehageligt, hvis du vil placere en blok kode der, som direkte behandler linjen, i stedet for at kalde en funktion, der behandler linjen; du bliver nødt til at gentage den kode, der er udsat for fejl.

I modsætning hertil behandler ProcessText3 linjen eller kalder kun den respektive funktion på ét sted, hvilket erstatter funktionen kalder ved hjælp af en kode blokerer en no-brainer. Dette koster to while betingelser i stedet for en. Bortset fra implementeringsforskellene opfører ProcessText3 nøjagtigt det samme som ProcessText1, bortset fra at det ikke betragter delen mellem det sidste nye linjetegn variablen og slutningen af variablen som linje, hvis den del er tom. Det vil sige, ProcessText3 går ikke i liniebehandling efter den sidste nye linjetegn i variablen, hvis dette nye linjetegn er det sidste tegn i variablen.

ProcessText2 fungerer ligesom ProcessText1, bortset fra at linjer skal have en efterfølgende ny linjetegn. Det vil sige, at delen mellem den sidste nye linjetegn i variablen og slutningen af variablen ikke betragtes som en linje og kastes lydløst væk. Derfor, hvis variablen ikke indeholder noget nyt linjetegn, sker der slet ingen liniebehandling.

Jeg kan godt lide den tilgang mere end de andre løsninger vist ovenfor, men sandsynligvis har jeg savnet noget (ikke meget erfaret med bash programmering og ikke meget interesseret i andre skaller).

Svar

Du kan bruge < < < til blot at læse fra variablen, der indeholder den nye linje -adskilte data:

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

Kommentarer

  • Velkommen til Unix & Linux! Dette gentager i det væsentlige et svar fra fire år siden. Send ikke et svar, medmindre du har noget nyt at bidrage med.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *