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

Ich habe eine Variable, die eine 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 

Antwort

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

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

Ein optimaler Weg zu Beim Lesen einer mehrzeiligen Variablen wird eine leere IFS -Variable und printf die Variable mit einem nachfolgenden Zeilenumbruch gesetzt:

# 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 einer Prozesssubstitution einer Pipe vorzuziehen, um [subtil] zu vermeiden. Erstellen einer Unterschale.

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

Kommentare

  • Wenn Sie alle Leerzeichen behalten möchten, verwenden Sie while IFS= read …. Wenn Sie Wenn Sie \ Interpretation verhindern möchten, verwenden Sie read -r
  • Ich ‚ habe die erwähnten Punkte fred.bear behoben und echo in printf %s, damit Ihr Skript auch mit nicht zahmen Eingaben funktioniert.
  • Um aus einer mehrzeiligen Variablen zu lesen, ist ein Herestring dem Piping von printf vorzuziehen (siehe l0b0 ‚ s Antwort).
  • @ata Obwohl ich ‚ dies gehört habe “ bevorzugt “ oft genug muss beachtet werden, dass ein Herestring immer erfordert, dass das Verzeichnis /tmp beschreibbar ist, da es vom Sein abhängt in der Lage, eine temporäre Arbeitsdatei zu erstellen. Sollten Sie sich jemals auf einem eingeschränkten System befinden, bei dem /tmp 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. G. mit der Pipe printf.
  • Im zweiten Beispiel, wenn die mehrzeilige Variable ‚ nicht a enthält Nach der neuen Zeile verlieren Sie das letzte Element. Ändern Sie es in: printf "%s\n" "$var" | while IFS= read -r line

Antwort

So verarbeiten Sie das Ausgabe einer Befehlszeile zeilenweise ( Erklärung ):

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

Wenn Sie die haben Daten bereits in einer Variablen:

printf %s "$foo" | … 

printf %s "$foo" ist fast identisch mit echo "$foo", gibt jedoch $foo buchstäblich aus, während echo "$foo" $foo als Option interpretieren könnte zum Echo-Befehl, wenn er mit einem - beginnt und in einigen Shells möglicherweise Backslash-Sequenzen in $foo 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 zum Zählen von Zeilen 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) einzufügen das benötigt den Wert von $n aus der Schleife) in einer Befehlsliste:

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

If Auf die nicht leeren Zeilen zu reagieren ist gut genug und die Eingabe ist nicht groß. Sie können die Wortaufteilung verwenden:

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

Erläuterung: Einstellung IFS in eine einzelne neue Zeile führt dazu, dass die Wortaufteilung nur in neuen Zeilen erfolgt (im Gegensatz zu Leerzeichen unter der Standardeinstellung). set -f deaktiviert das Globbing (dh die Platzhaltererweiterung), was andernfalls zum Ergebnis einer Befehlssubstitution $(jobs) oder einer Variablensubstitution $foo. Die for -Schleife wirkt auf alle Teile von $(jobs), die alle nicht leeren Zeilen in der Befehlsausgabe sind. Stellen Sie abschließend die Einstellungen für globbing und IFS auf Werte zurück, die den Standardeinstellungen entsprechen.

Kommentare

  • Ich hatte Probleme beim Einstellen des IFS und beim Deaktivieren des 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 kehren Sie damit zum ursprünglichen Verhalten zurück.
  • @BjornRoche: Verwenden Sie in einer Funktion local IFS=something. ‚ hat keinen Einfluss auf den globalen Gültigkeitsbereich. IIRC, unset IFS bringt ‚ Sie nicht zum Standard zurück (und sicherlich nicht ‚ funktioniert nicht, wenn es nicht ‚ war (t vorher).
  • Ich frage mich, ob die Verwendung von set im Weg ist Das im letzten Beispiel gezeigte ist korrekt.Das Code-Snippet geht davon aus, dass set +f zu Beginn aktiv war, und stellt daher diese Einstellung am Ende wieder her. Diese Annahme könnte jedoch falsch sein. Was ist, wenn set -f zu Beginn aktiv war?
  • @Binarus Ich stelle nur Einstellungen wieder her, die den Standardeinstellungen entsprechen. Wenn Sie die ursprünglichen Einstellungen wiederherstellen möchten, müssen Sie mehr Arbeit leisten. Speichern Sie für set -f das Original $-. Für IFS ist ‚ ärgerlich fummelig, wenn Sie ‚ keine local und Sie möchten den nicht gesetzten Fall unterstützen; Wenn Sie es wiederherstellen möchten, empfehle ich, die Invariante zu erzwingen, dass IFS gesetzt bleibt.
  • Verwenden von local Dies ist in der Tat die beste Lösung, da local - die Shell-Optionen lokalisiert und local IFS IFS lokal. Leider ist local nur innerhalb von Funktionen gültig, was eine Umstrukturierung des Codes erforderlich macht. Ihr Vorschlag, die Richtlinie einzuführen, dass IFS immer festgelegt ist, klingt ebenfalls sehr vernünftig und löst den größten Teil des Problems. Danke!

Antwort

Problem: Wenn Sie die while-Schleife verwenden, wird sie in einer Unterschale ausgeführt und alle Variablen werden ausgeführt hat 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= 

Kommentare

  • VIELEN DANK !! Alle oben genannten Lösungen sind für mich fehlgeschlagen.
  • Piping in eine while read -Schleife in bash bedeutet, dass sich die while-Schleife in einer Unterschale befindet, sodass Variablen nicht ‚ t global. while read;do ;done <<< "$var" macht den Schleifenkörper nicht zu einer Unterschale. (Letzte Bash hat die Option, den Body einer cmd | while -Schleife nicht in eine Subshell zu setzen, wie es ksh immer getan hat.)
  • Siehe auch dieser verwandte Beitrag .
  • In ähnlichen Situationen fand ich es überraschend schwierig, IFS richtig zu behandeln. Diese Lösung hat auch ein Problem: Was ist, wenn IFS am Anfang überhaupt nicht gesetzt ist (d. H. Undefiniert ist)? Es wird in jedem Fall nach diesem Code-Snippet definiert; ‚ scheint nicht korrekt zu sein.

Antwort

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

Referenzen:

Kommentare

  • -r ist ebenfalls 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)
  • Nur diese Lösung hat bei mir funktioniert. Vielen Dank, Brah.
  • ‚ Diese Lösung leidet nicht unter demselben Problem, das in den Kommentaren zu @dogbane ‚ s? Was passiert, wenn die letzte Zeile der Variablen nicht durch ein Zeilenumbruchzeichen abgeschlossen wird?
  • Diese Antwort bietet die sauberste Möglichkeit, den Inhalt einer Variablen der while read zuzuführen Konstrukt.

Antwort

Verwenden Sie in neueren Bash-Versionen mapfile oder readarray zum effizienten Lesen der Befehlsausgabe in Arrays

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

Haftungsausschluss: Schreckliches Beispiel, aber Sie können häufig kommen mit einem besseren Befehl als Sie selbst

Kommentare

  • Es ‚ ist eine schöne Art, aber Würfe / var / tmp mit temporären Dateien auf meinem System. +1 sowieso
  • @eugene: das ‚ ist lustig. Auf welchem System (Distribution / OS) ist das?
  • Es

ist FreeBSD 8. So reproduzieren Sie: putreadarrayin einer Funktion und rufe die Funktion einige Male auf.

  • Schön, @sehe. +1
  • Antwort

    Die allgemeinen Muster zur Lösung dieses Problems wurden in den anderen Antworten angegeben.

    Ich möchte jedoch meinen Ansatz hinzufügen, obwohl ich nicht sicher bin, wie effizient er ist. Aber er ist (zumindest für mich) durchaus verständlich und ändert nichts an der ursprünglichen Variablen (alle Lösungen, die read muss die betreffende Variable mit einem abschließenden Zeilenumbruch haben und daher hinzufügen, wodurch die Variable geändert wird. Es werden keine Unterschalen erstellt (was bei allen Pipe-basierten Lösungen der Fall ist), die hier nicht verwendet werden -strings (die ihre eigenen Probleme haben) und keine Prozessersetzung verwenden (nichts dagegen, aber manchmal etwas schwer zu verstehen).

    Eigentlich verstehe ich nicht, warum “ werden so selten verwendet.Vielleicht sind sie nicht portabel, aber da das OP das bash -Tag verwendet hat, wird mich das nicht aufhalten 🙂

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

    Ausgabe:

    root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

    Einige Hinweise:

    Das Verhalten hängt davon ab, welche Variante von ProcessText, die Sie verwenden. Im obigen Beispiel habe ich ProcessText1 verwendet.

    Beachten Sie, dass

    • ProcessText1 hält Zeilenumbrüche am Zeilenende
    • ProcessText1 verarbeitet die letzte Zeile der Variablen (die den Text enthält) c3) obwohl diese Zeile kein nachfolgendes Zeilenumbruchzeichen enthält. Aufgrund des fehlenden nachgestellten Zeilenumbruchs wird die Eingabeaufforderung nach der Skriptausführung an die letzte angehängt Zeile der Variablen, ohne von der Ausgabe getrennt zu sein.
    • ProcessText1 betrachtet den Teil zwischen der letzten neuen Zeile in der Variablen und dem Ende der Variablen immer als Zeile , auch wenn es leer ist; Natürlich hat diese Zeile, ob leer oder nicht, kein nachfolgendes Zeilenumbruchzeichen. Das heißt, selbst wenn das letzte Zeichen in der Variablen eine neue Zeile ist, behandelt ProcessText1 den leeren Teil (Nullzeichenfolge) zwischen dieser letzten neuen Zeile und dem Ende der Variablen als (noch) leere) Zeile und wird an die Zeilenverarbeitung übergeben. Sie können dieses Verhalten leicht verhindern, indem Sie den zweiten Aufruf von ProcessLine in eine geeignete Check-if-Empty-Bedingung einschließen. Ich halte es jedoch für logischer, es unverändert zu lassen.

    ProcessText1 muss ProcessLine an zwei Stellen, was unangenehm sein kann, wenn Sie dort einen Codeblock platzieren möchten, der die Zeile direkt verarbeitet, anstatt eine Funktion aufzurufen, die die Zeile verarbeitet. Sie müssten den fehleranfälligen Code wiederholen.

    Im Gegensatz dazu verarbeitet ProcessText3 die Zeile oder ruft die entsprechende Funktion nur an einer Stelle auf und ersetzt sie Der Funktionsaufruf durch einen Codeblock ist ein Kinderspiel. Dies geht zu Lasten von zwei while Bedingungen anstelle von einer. Abgesehen von den Implementierungsunterschieden verhält sich ProcessText3 genauso wie ProcessText1, außer dass der Teil zwischen dem letzten Zeilenumbruchzeichen in nicht berücksichtigt wird Die Variable und das Ende der Variablen als Zeile, wenn dieser Teil leer ist. Das heißt, ProcessText3 wird nach dem letzten Zeilenumbruchzeichen der Variablen nicht in die Zeilenverarbeitung aufgenommen, wenn dieses Zeilenumbruchzeichen das letzte Zeichen in der Variablen ist.

    ProcessText2 funktioniert wie ProcessText1, außer dass Zeilen ein nachfolgendes Zeilenumbruchzeichen haben müssen. Das heißt, der Teil zwischen dem letzten Zeilenumbruchzeichen in der Variablen und dem Ende der Variablen wird nicht als Zeile betrachtet und stillschweigend weggeworfen. Wenn die Variable kein Zeilenumbruchzeichen enthält, findet daher überhaupt keine Zeilenverarbeitung statt.

    Ich mag diesen Ansatz mehr als die anderen oben gezeigten Lösungen, aber wahrscheinlich habe ich etwas verpasst (nicht sehr erfahren in bash programmieren und sich nicht sehr für andere Shells interessieren).

    Antwort

    Mit < < < können Sie einfach aus der Variablen lesen, die die neue Zeile enthält -getrennte Daten:

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

    Kommentare

    • 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.

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.