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
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, atset +f
var aktiv i starten, og gendanner derfor denne indstilling i slutningen. Denne antagelse kan dog være forkert. Hvad hvisset -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$-
. ForIFS
er det ‘ irriterende, hvis du ikke ‘ t harlocal
og du vil understøtte den usættede sag; hvis du vil gendanne det, anbefaler jeg at håndhæve den uændrede, atIFS
forbliver indstillet. - Brug af
local
ville faktisk være den bedste løsning, fordilocal -
gør shell-mulighederne lokale, oglocal IFS
gørIFS
lokal. Desværre erlocal
kun gyldig inden for funktioner, hvilket gør kodestrukturering nødvendig. Dit forslag om at indføre politikken om, atIFS
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 encmd | 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 hvisIFS
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 tekstenc3
) 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, behandlerProcessText1
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 tilProcessLine
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.
while IFS= read
…. Hvis du ønsker at forhindre \ fortolkning, brug derefterread -r
echo
tilprintf %s
, så dit script fungerer selv med ikke-tamme input./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. medprintf
-røret.printf "%s\n" "$var" | while IFS= read -r line