Zählerinkrement in Bash-Schleife funktioniert nicht

125

Ich habe das folgende einfache Skript, in dem ich eine Schleife ausführe und eine pflegen möchte COUNTER. Ich kann nicht herausfinden, warum der Zähler nicht aktualisiert wird. Liegt es an der Subshell, die erstellt wird? Wie kann ich das möglicherweise beheben?

#!/bin/bash

WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' | awk -F ', ' '{print $2,$4,$0}' | awk '{print "http://domain.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' | awk -F '&end=1' '{print $1"&end=1"}' |
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
)

echo $COUNTER # output = 0
Sparsh Gupta
quelle
1
Siehe auch
Gabriel Devillers
Sie müssen die while-Schleife nicht in die Subshell einfügen. Entfernen Sie einfach die Klammern während der Schleife, es reicht aus. Wenn Sie die Schleife in die Subshell einfügen müssen, geben Sie den Zähler nach Abschluss des Vorgangs einmal in die temporäre Datei ein und stellen Sie diese Datei außerhalb der Subshell wieder her. Ich werde Ihnen als Antwort das endgültige Verfahren vorbereiten.
Znik

Antworten:

156

Erstens erhöhen Sie den Zähler nicht. Ändern COUNTER=$((COUNTER))in COUNTER=$((COUNTER + 1))oder COUNTER=$[COUNTER + 1]wird es erhöhen.

Zweitens ist es schwieriger, Subshell-Variablen an den Angerufenen zurückzugeben, wie Sie vermuten. Variablen in einer Unterschale sind außerhalb der Unterschale nicht verfügbar. Dies sind lokale Variablen für den untergeordneten Prozess.

Eine Möglichkeit, dies zu lösen, besteht darin, eine temporäre Datei zum Speichern des Zwischenwerts zu verwenden:

TEMPFILE=/tmp/$$.tmp
echo 0 > $TEMPFILE

# Loop goes here
  # Fetch the value and increase it
  COUNTER=$[$(cat $TEMPFILE) + 1]

  # Store the new value
  echo $COUNTER > $TEMPFILE

# Loop done, script done, delete the file
unlink $TEMPFILE
bos
quelle
30
$ [...] ist veraltet.
Chepper
1
@chepner Haben Sie eine Referenz, die besagt, dass sie $[...]veraltet ist? Gibt es eine alternative Lösung?
Blong
9
$[...]wurde von verwendet, bashbevor $((...))von der POSIX-Shell übernommen wurde. Ich bin nicht sicher, ob es jemals offiziell veraltet war, aber ich kann es in der bashManpage nicht erwähnen , und es scheint nur aus Gründen der Abwärtskompatibilität unterstützt zu werden.
Chepner
Auch ist $ (...) bevorzugt über...
Lennart Rolland
7
@blong Hier ist eine SO-Frage zu $ ​​[...] vs $ ((...)), die die Ablehnung
Ogre Psalm33
87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

GEPRÜFTE BASH: Centos, SuSE, RH

Jay Stan
quelle
1
@kroonwijk Es muss ein Leerzeichen vor der eckigen Klammer stehen (um die Wörter formal abzugrenzen). Bash kann das Ende des vorherigen Ausdrucks sonst nicht sehen.
EdwardGarson
1
Die Fragen waren ungefähr eine Weile mit einer Pipe. Wenn also eine Subshell erstellt wird, ist Ihre Antwort richtig, aber Sie verwenden keine Pipe, sodass die Frage nicht beantwortet wird
Chrisweb
2
Laut dem Kommentar von chepner zu einer anderen Antwort ist die $[ ]Syntax veraltet. stackoverflow.com/questions/10515964/…
Mark Haferkamp
Dies löst die Hauptfrage nicht, die Hauptschleife wird unter die Unterschale gestellt
Znik
42
COUNTER=$((COUNTER+1)) 

ist ein ziemlich ungeschicktes Konstrukt in der modernen Programmierung.

(( COUNTER++ ))

sieht "moderner" aus. Sie können auch verwenden

let COUNTER++

wenn Sie denken, dass dies die Lesbarkeit verbessert. Manchmal gibt Bash zu viele Möglichkeiten, Dinge zu tun - Perl-Philosophie, nehme ich an -, wenn der Python "es gibt nur einen richtigen Weg, dies zu tun" vielleicht angemessener ist. Das ist eine umstrittene Aussage, wenn es jemals eine gab! Wie auch immer, ich würde vorschlagen, dass das Ziel (in diesem Fall) nicht nur darin besteht, eine Variable zu erhöhen, sondern (allgemeine Regel) auch Code zu schreiben, den jemand anderes verstehen und unterstützen kann. Konformität trägt wesentlich dazu bei.

HTH

Bill Parker
quelle
Dies geht nicht auf die ursprüngliche Frage ein, wie der aktualisierte Wert im Zähler nach Beendigung der (Unterprozess-) Schleife abgerufen werden kann
Luis Vazquez
16

Versuchen zu benutzen

COUNTER=$((COUNTER+1))

anstatt

COUNTER=$((COUNTER))
dbf
quelle
8
oder einfachlet "COUNTER++"
nullpotent
2
Entschuldigung, es war ein Tippfehler. Es ist tatsächlich ((COUNTER + 1))
Sparsh Gupta
8
@ AaronDigulla: (( COUNTER++ ))(kein Dollarzeichen)
Bis auf weiteres angehalten.
2
Ich bin mir nicht sicher warum, aber ich sehe, dass ein Skript von mir bei der Verwendung wiederholt fehlschlägt, (( COUNTER++ ))aber als ich darauf umgestiegen bin, hat COUNTER=$((COUNTER + 1))es funktioniert. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu
Vielleicht läuft Ihre Hash-Bang-Linie bash als / bin / sh anstelle von / bin / bash?
Max
12

Ich denke, dieser einzelne awk-Aufruf entspricht Ihrer grep|grep|awk|awkPipeline: Bitte testen Sie ihn. Ihr letzter awk-Befehl scheint überhaupt nichts zu ändern.

Das Problem mit COUNTER ist, dass die while-Schleife in einer Subshell ausgeführt wird, sodass alle Änderungen an der Variablen beim Beenden der Subshell verschwinden. Sie müssen auf den Wert von COUNTER in derselben Subshell zugreifen. Oder befolgen Sie den Rat von @ DennisWilliamson, verwenden Sie eine Prozessersetzung und vermeiden Sie die Subshell insgesamt.

awk '
  /GET \/log_/ && /upstream timed out/ {
    split($0, a, ", ")
    split(a[2] FS a[4] FS $0, b)
    print "http://example.com" b[5] "&ip=" b[2] "&date=" b[7] "&time=" b[8] "&end=1"
  }
' | {
    while read WFY_URL
    do
        echo $WFY_URL #Some more action
        (( COUNTER++ ))
    done
    echo $COUNTER
}
Glenn Jackman
quelle
1
Danke, das letzte awk wird im Grunde alles nach end = 1 entfernen und ein neues end = 1 an das Ende setzen (damit wir beim nächsten Mal alles entfernen können, was danach angehängt wird).
Sparsh Gupta
1
@SparshGupta, die vorherige awk druckt nichts nach "end = 1".
Glenn Jackman
Dies verbessert das Fragenskript sehr gut, löst jedoch nicht das Problem mit dem Erhöhen des Zählers in der Subshell
Znik
12
count=0   
base=1
(( count += base ))
pkm
quelle
11

Anstatt eine temporäre Datei zu verwenden, können Sie whilemithilfe der Prozessersetzung vermeiden, eine Unterschale um die Schleife zu erstellen .

while ...
do
   ...
done < <(grep ...)

Übrigens sollten Sie in der Lage sein, all das grep, grep, awk, awk, awkin ein einziges zu verwandeln awk.

Ab Bash 4.2 gibt es eine lastpipeOption, die

führt den letzten Befehl einer Pipeline im aktuellen Shell-Kontext aus. Die Lastpipe-Option hat keine Auswirkung, wenn die Jobsteuerung aktiviert ist.

bash -c 'echo foo | while read -r s; do c=3; done; echo "$c"'

bash -c 'shopt -s lastpipe; echo foo | while read -r s; do c=3; done; echo "$c"'
3
Bis auf weiteres angehalten.
quelle
Die Prozessersetzung ist großartig, wenn Sie einen Zähler innerhalb der Schleife inkrementieren und nach Abschluss außerhalb verwenden möchten. Das Problem bei der Prozessersetzung besteht darin, dass ich keine Möglichkeit gefunden habe, auch den Statuscode des ausgeführten Befehls abzurufen, was bei Verwendung einer Pipe möglich ist mit $ {PIPESTATUS [*]}
chrisweb
@chrisweb: Ich habe Informationen über hinzugefügt lastpipe. Übrigens sollten Sie wahrscheinlich verwenden "${PIPESTATUS[@]}"(at anstelle von Sternchen).
Bis auf weiteres angehalten.
Errata. In Bash (nicht in Perl, wie ich es versehentlich geschrieben habe) ist der Exit-Code eine Tabelle, dann können Sie alle Exit-Codes in der Pipe-Kette separat überprüfen. Vor dem ersten Test muss Ihr Schritt diese Tabelle kopieren. Andernfalls gehen nach dem ersten Befehl alle Werte verloren.
Znik
Dies ist die Lösung, die für mich funktioniert hat und ohne eine externe Datei zum Speichern des Variablenwerts zu verwenden, der meiner Meinung nach zu viel Fußgänger ist.
Luis Vazquez
8

minimalistisch

counter=0
((counter++))
echo $counter
geekzspot
quelle
Einfache Sache :-). Danke @geekzspot
Hussain K
funktioniert zum Beispiel nicht in Frage, weil es Subshell gibt
Znik
3

Das ist alles was Sie tun müssen:

$((COUNTER++))

Hier ist ein Auszug aus Learning the Bash Shell , 3. Auflage, S. 147, 148:

Bash- Arithmetik-Ausdrücke entsprechen ihren Gegenstücken in den Sprachen Java und C. [9] Vorrang und Assoziativität sind dieselben wie in C. Tabelle 6-2 zeigt die unterstützten arithmetischen Operatoren. Obwohl einige davon Sonderzeichen sind (oder enthalten), müssen sie nicht mit einem Backslash-Escapezeichen versehen werden, da sie innerhalb der Syntax $ ((...)) liegen.

..........................

Die Operatoren ++ und - sind nützlich, wenn Sie einen Wert um eins erhöhen oder verringern möchten. [11] Sie funktionieren auf die gleiche wie in Java und C, zB Wert ++ Schritte Wert von 1. Dieses genannt wird Nachinkrement ; Es gibt auch ein Pre-Inkrement : ++ Wert . Der Unterschied wird anhand eines Beispiels deutlich:

$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

Siehe http://www.safaribooksonline.com/a/learning-the-bash/7572399/

CE Montijo
quelle
Dies ist die Version davon, die ich brauchte, weil ich sie unter der Bedingung einer ifAussage verwendet habe: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi Richtig oder falsch, dies ist die einzige Version, die zuverlässig funktioniert hat.
LS
Was an diesem Formular wichtig ist, ist, dass Sie ein Inkrement in einem einzigen Schritt verwenden können. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky
1
@LS: if (( needsComma++ > 0 )); thenoderif (( needsComma++ )); then
Bis auf weiteres angehalten.
Wenn ich "echo $ ((i ++))" in bash verwende, erhalte ich immer "/opt/xyz/init.sh: Zeile 29: i: Befehl nicht gefunden" Was mache ich falsch?
mmo
Damit wird die Frage nach dem Abrufen des Zählerwerts außerhalb der Schleife nicht beantwortet.
Luis Vazquez
1

Dies ist ein einfaches Beispiel

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done
zwitterion
quelle
1
einfaches Beispiel, aber nicht anwendbar auf Fragen.
Znik
0

Es scheint, dass Sie counterdas Skript nicht aktualisiert habencounter++

yjshen
quelle
Entschuldigung für den Tippfehler, ich benutze tatsächlich ((COUNTER + 1)) in einem Skript, das nicht funktioniert
Sparsh Gupta
Es ist egal, ob es um den Wert + 1 oder um den Wert ++ erhöht wird. Nach dem Ende der Unterschale geht der Zählerwert verloren und wird auf den anfänglichen 0-Wert zurückgesetzt, der beim Start dieses Skripts festgelegt wurde.
Znik
0

Es gab zwei Bedingungen, die dazu führten, dass der Ausdruck ((var++))für mich fehlschlug:

  1. Wenn ich bash auf den strengen Modus ( set -euo pipefail) setze und mein Inkrement bei Null (0) beginne .

  2. Das Beginnen bei eins (1) ist in Ordnung, aber Null bewirkt, dass das Inkrement "1" zurückgibt, wenn "++" ausgewertet wird, was im strengen Modus ein Fehlercode für den Rückkehrcode ungleich Null ist.

Ich kann dieses Verhalten entweder nutzen ((var+=1))oder ihm var=$((var+1))entkommen

Augustus Hill
quelle
0

Das Quellenskript hat ein Problem mit der Subshell. Erstes Beispiel: Sie benötigen wahrscheinlich keine Unterschale. Aber wir wissen nicht, was unter "Noch mehr Action" verborgen ist. Die beliebteste Antwort hat einen versteckten Fehler, der die E / A erhöht und nicht mit Subshell funktioniert, da der Couter innerhalb der Schleife wiederhergestellt wird.

Fügen Sie kein '\' - Zeichen hinzu, da dies den Bash-Interpreter über die Fortsetzung der Leitung informiert. Ich hoffe es wird dir oder irgendjemandem helfen. Aber meiner Meinung nach sollte dieses Skript vollständig in ein AWK-Skript konvertiert oder mit Regexp oder Perl in Python umgeschrieben werden, aber die Popularität von Perl über Jahre hinweg wird beeinträchtigt. Mach es besser mit Python.

Korrigierte Version ohne Subshell:

#!/bin/bash
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
#(  #unneeded bracket
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
# ) unneeded bracket

echo $COUNTER # output = 0

Version mit Subshell, wenn es wirklich benötigt wird

#!/bin/bash

TEMPFILE=/tmp/$$.tmp  #I've got it from the most popular answer
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
echo $COUNTER > $TEMPFILE  #store counter only once, do it after loop, you will save I/O
)

COUNTER=$(cat $TEMPFILE)  #restore counter
unlink $TEMPFILE
echo $COUNTER # output = 0
Znik
quelle