bash: Variable verliert am Ende der while-Leseschleife an Wert

36

Ich habe ein Problem in einem meiner Shell-Skripte. Gefragt ein paar Kollegen, aber sie alle schütteln nur den Kopf (nach einigem Kratzen), also bin ich hierher gekommen, um eine Antwort zu bekommen.

Nach meinem Verständnis sollte das folgende Shell-Skript "Count is 5" als letzte Zeile ausgeben. Außer es tut nicht. Es wird "Count is 0" ausgegeben. Wenn das "while read" durch eine andere Art von Schleife ersetzt wird, funktioniert es einwandfrei. Hier ist das Skript:

Echo "1"> input.data
Echo "2" >> input.data
Echo "3" >> input.data
Echo "4" >> input.data
Echo "5" >> input.data

CNT = 0 

cat input.data | beim Lesen;
machen
  let CNT ++;
  echo "Zählen bis $ CNT"
getan 
echo "Count is $ CNT"

Warum passiert das und wie kann ich es verhindern? Ich habe dies in Debian Lenny und Squeeze ausprobiert, das gleiche Ergebnis (dh Bash 3.2.39 und Bash 4.1.5. Ich gebe voll zu, kein Shellskript-Assistent zu sein, daher wären alle Hinweise willkommen.

wolfgangsz
quelle

Antworten:

30

Siehe Argument @ Bash FAQ-Eintrag Nr. 24: "Ich setze Variablen in einer Schleife. Warum verschwinden sie plötzlich, nachdem die Schleife beendet wurde? Oder warum kann ich keine Daten zum Lesen weiterleiten?" (zuletzt hier archiviert ).

Zusammenfassung: Dies wird nur ab Bash 4.2 und höher unterstützt. Wenn Sie bash verwenden, müssen Sie verschiedene Methoden wie Befehlsersetzungen anstelle einer Pipe verwenden.

Ignacio Vazquez-Abrams
quelle
Sie erhalten den Bonus, da ich durch Ihre Antwort die meisten Möglichkeiten hatte.
Wolfgangsz
5
Der Link ist tot. Dies ist der Grund, warum Antworten nur über Links schlecht sind. Fassen Sie zumindest die Antwort hier zusammen.
Rudolfbyker
Gott, noch ein anderes Mal, wo ksh einfach so viel besser ist ... warum, nur warum haben sich alle um bash geschart.
Florian Heigl
@FlorianHeigl: Behauptest du, dass ksh die Eine Wahre Shell ist?
Ignacio Vazquez-Abrams
@ IgnacioVazquez-Abrams nein, aber ich behaupte, dass die while-Schleife in bash eine schreckliche PITA ist. Das Loop-Handling war der Dauerbrenner, der es daran hinderte, die Funktionalität von 1993 zu erreichen. Die anderen Dinge sind getopt handling, wo der (ebenfalls 1993) eingebaute Handler einfach und fähig war, etwas, das man immer noch nicht bekommen kann, wenn man nicht zB docopt benutzt. Ich behaupte, dass sich bash seit über 20 Jahren selbst in die Knie gezwungen hat und dass die Zeit, die für DIESES HIER aufgewendet wird oder Millionen von schlechten Getopts verwendet werden, unermesslich ist - nur akzeptiert, weil die meisten Menschen es nie erfahren werden.
Florian Heigl
30

Dies ist eine Art "häufiger" Fehler. Pipes erstellen SubShells, so dass das while readauf einer anderen Shell als Ihrem Skript ausgeführt wird, sodass sich Ihre CNTVariable niemals ändert (nur die in der Pipe-Subshell).

echoGruppieren whileSie die letzte mit der Unterschale , um sie zu reparieren (es gibt viele andere Möglichkeiten, um sie zu reparieren, dies ist eine. Iain und Ignacios Antworten haben andere.)

CNT=0

 cat input.data | ( while read 
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT" )

Lange Erklärung:

  1. Sie deklarieren CNTin Ihrem Skript den Wert 0;
  2. Eine SubShell wird am |to gestartet while read;
  3. Ihre $CNTVariable wird mit dem Wert 0 in die SubShell exportiert.
  4. Die SubShell zählt und erhöht den CNTWert auf 5.
  5. SubShell wird beendet, Variablen und Werte werden zerstört (sie kehren nicht zum aufrufenden Prozess / Skript zurück).
  6. Sie echoIhren ursprünglichen CNTWert von 0.
Core-Dump
quelle
2
Das erste Shellskript, das ich jemals geschrieben habe, gab mir die gleichen Probleme. Ich stieß eine Weile mit dem Kopf gegen die Wand, bevor ich herausfand, dass diese Pfeifen zusätzliche Shells hervorbringen. Jede Variable, mit der Sie in einer Pipe arbeiten, verlässt den Gültigkeitsbereich, sobald die Pipe endet. Wenn Sie also wirklich etwas mit einer Variablen außerhalb der Pipe tun möchten, in der sie verwendet wurde, müssen Sie dies tun Halten Sie den Status durch etwas Funky wie eine temporäre Datei.
Photoionisierte
Hervorragende Antwort, leider kann ich nur einen Annahmebonus geben. Es tut uns leid.
Wolfgangsz
10

Das funktioniert

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 unterstützt GoFundMonica
quelle
Ich mag das auf intelligente Weise, weil Sie wissen, wo sich die benötigten Daten befinden und sie nur zurückholen müssen. Wenn Sie keine hochqualifizierten Lösungen kennen, können Sie immer eine Datei "lesen", hahahha. +1 für dich.
m3nda
1
Wenn Sie dies lesen, beachten Sie, dass die von Iain bereitgestellte Lösung nur funktioniert, wenn Ihr Skript Bash explizit aufruft, indem Sie die erste Zeile: #! / Bin / bash und Folgendes eingeben: #! / Bin / sh wird nicht funktionieren.
Roadowl
1
Interessant, ich habe zum ersten Mal gesehen, dass eine nutzlose Verwendung von Cat tatsächlich die Funktionsfähigkeit von Code verhinderte . Übrigens ist @Roadowl hier der einzige Bashismus die Zeile, let CNT++die stattdessen verwendet werden sollte CNT="$((CNT+1))", um eine POSIX-konforme arithmetische Erweiterung zu verwenden . Der Rest ist bereits portabel.
Wildcard
6

Versuchen Sie stattdessen, die Daten in einer Sub-Shell zu übergeben, wie es eine Datei vor der while-Schleife ist. Dies ähnelt der Lösung von lain, setzt jedoch voraus, dass Sie keine intermittierenden Dateien benötigen:

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"
Steve
quelle