Wie kann ich Zeile für Zeile aus einer Variablen in Bash lesen?

48

Ich habe eine Variable, die mehrzeilige Ausgabe eines Befehls enthält. Was ist die effizienteste Methode, um die Ausgabe zeilenweise aus der Variablen zu lesen?

Zum Beispiel:

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

Antworten:

64

Sie können eine while-Schleife mit Prozessersetzung verwenden:

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

Eine optimale Methode zum Lesen einer mehrzeiligen Variablen besteht darin, eine leere IFSVariable printffestzulegen und die Variable mit einem nachgestellten Zeilenumbruch zu versehen:

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

Hinweis: Gemäß Shellcheck sc2031 ist die Verwendung von Prozesssubition einer Pipe vorzuziehen, um das [subtile] Erstellen einer Subshell zu vermeiden.

Beachten Sie außerdem, dass die Benennung der Variablen jobszu Verwirrung führen kann, da dies auch der Name eines allgemeinen Shell-Befehls ist.

Dogbane
quelle
3
Wenn Sie alle Leerzeichen while IFS= readread -r
beibehalten
Ich habe die Punkte festgelegt fred.bear erwähnt, sowie geändert echozu printf %s, so dass Ihr Skript funktionieren würde, auch bei nicht zahm Eingang.
Gilles 'SO - hör auf böse zu sein'
Um aus einer mehrzeiligen Variablen zu lesen, ist ein Herestring einem Piping aus printf vorzuziehen (siehe die Antwort von 10b0).
ata
1
@ata Obwohl ich das "vorzuziehen" oft genug gehört habe, muss beachtet werden, dass für einen Herestring immer ein beschreibbares Verzeichnis erforderlich ist /tmp, da es darauf ankommt , dass eine temporäre Arbeitsdatei erstellt werden kann. Sollten Sie sich jemals in einem eingeschränkten System befinden /tmp, das schreibgeschützt ist (und von Ihnen nicht geändert werden kann), werden Sie sich über die Möglichkeit freuen, eine alternative Lösung zu verwenden, z printf. B. mit der Pipe.
Syntaxfehler
1
Wenn die mehrzeilige Variable im zweiten Beispiel keinen abschließenden Zeilenumbruch enthält, geht das letzte Element verloren. Ändern Sie es auf:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett
26

So verarbeiten Sie die Ausgabe eines Befehls zeilenweise ( Erläuterung ):

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

Wenn Sie die Daten bereits in einer Variablen haben:

printf %s "$foo" | 

printf %s "$foo"ist fast identisch mit echo "$foo", gibt jedoch $foobuchstäblich aus, wobei dies als Option für den Echo-Befehl echo "$foo"interpretiert werden kann, $foowenn er mit einem beginnt -, und möglicherweise die Backslash-Sequenzen $fooin einigen Shells erweitert.

Beachten Sie, dass in einigen Shells (ash, bash, pdksh, aber nicht ksh oder zsh) die rechte Seite einer Pipeline in einem separaten Prozess ausgeführt wird, sodass alle in der Schleife festgelegten Variablen verloren gehen. Das folgende Skript für die Zeilenzählung gibt beispielsweise 0 in diesen Shells aus:

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

Eine Problemumgehung besteht darin, den Rest des Skripts (oder zumindest den Teil, der den Wert $naus der Schleife benötigt) in eine Befehlsliste einzufügen:

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

Wenn das Eingreifen in die nicht leeren Zeilen gut genug ist und die Eingabe nicht sehr umfangreich ist, können Sie die Wortteilung verwenden:

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

Erläuterung: IFSWenn Sie eine einzelne Zeile als Zeilenumbruch festlegen, wird das Wort nur in Zeilenumbrüchen aufgeteilt (im Gegensatz zu Leerzeichen in der Standardeinstellung). set -fDeaktiviert das Globbing (dh die Platzhaltererweiterung), das andernfalls bei einer Befehlsersetzung $(jobs)oder einer Variablenersetzung auftreten würde $foo. Die forSchleife wirkt auf alle Teile von $(jobs), also alle nicht leeren Zeilen in der Befehlsausgabe. Stellen Sie abschließend die Globbing- IFSEinstellungen wieder her.

Gilles 'SO - hör auf böse zu sein'
quelle
Ich hatte Probleme beim Einstellen von IFS und beim Deaktivieren von IFS. Ich denke, das Richtige ist, den alten Wert von IFS zu speichern und IFS auf diesen alten Wert zurückzusetzen. Ich bin kein Bash-Experte, aber meiner Erfahrung nach werden Sie auf das ursprüngliche Verhalten zurückgebracht.
Bjorn Roche
1
@ BjornRoche: innerhalb einer Funktion verwenden local IFS=something. Der Wert für den globalen Bereich wird nicht beeinflusst. IIRC unset IFSbringt Sie nicht auf die Standardeinstellungen zurück (und funktioniert auf keinen Fall, wenn dies vorher nicht die Standardeinstellungen war).
Peter Cordes
14

Problem: Wenn Sie eine while-Schleife verwenden, wird diese in der Subshell ausgeführt und alle Variablen gehen verloren. Lösung: Verwenden Sie für Schleife

# 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=
Dima
quelle
ICH DANKE DIR SEHR!! Alle oben genannten Lösungen sind für mich gescheitert.
hax0r_n_code
Piping in eine while readSchleife in Bash bedeutet, dass sich die While-Schleife in einer Subshell befindet, sodass Variablen nicht global sind. while read;do ;done <<< "$var"macht den Schleifenkörper nicht zu einer Unterschale. (Letzte Bash hat eine Option, um den Körper einer cmd | whileSchleife nicht in eine Unterschale zu setzen, wie ksh es immer getan hat.)
Peter Cordes
Siehe auch diesen verwandten Beitrag .
Wildcard
10

Verwenden Sie in neueren Bash-Versionen mapfileoder readarray, um die Befehlsausgabe effizient in Arrays zu lesen

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

Haftungsausschluss: Ein schreckliches Beispiel, aber Sie können sich durchaus einen besseren Befehl einfallen lassen als Sie

sehe
quelle
Es ist eine schöne Möglichkeit, aber Würfe / var / tmp mit temporären Dateien auf meinem System. +1 sowieso
Eugene Yarmash
@Eugene: Das ist lustig. Auf welchem ​​System (Distribution / OS) läuft das?
sehe
Es ist FreeBSD 8. So reproduzieren Sie: Fügen Sie readarrayeine Funktion ein und rufen Sie die Funktion einige Male auf.
Eugene Yarmash
Schön, @sehe. +1
Teresa e Junior
7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Verweise:

l0b0
quelle
3
-rist auch eine gute Idee; Es verhindert, dass \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(was wichtig ist, um den Verlust von Leerzeichen zu verhindern)
Peter.O
-1

Mit <<< können Sie einfach aus der Variablen lesen, die die durch Zeilenumbrüche getrennten Daten enthält:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"
Matthew Herrmann
quelle
1
Willkommen bei Unix & Linux! Dies entspricht im Wesentlichen einer Antwort von vor vier Jahren. Bitte posten Sie keine Antwort, es sei denn, Sie haben etwas Neues beizutragen.
G-Man sagt, dass Monica