L'incrément du compteur dans la boucle Bash ne fonctionne pas

125

J'ai le script simple suivant dans lequel j'exécute une boucle et je souhaite maintenir un fichier COUNTER. Je n'arrive pas à comprendre pourquoi le compteur ne se met pas à jour. Est-ce dû à un sous-shell créé? Comment puis-je potentiellement résoudre ce problème?

#!/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
la source
1
En relation: stackoverflow.com/questions/13726764/…
Gabriel Devillers
Vous n'avez pas besoin de mettre une boucle while dans le sous-shell. Retirez simplement les crochets autour de la boucle while, c'est suffisant. Ou bien si vous devez le mettre en boucle dans le sous-shell, puis après un certain temps, vidangez le compteur dans le fichier temporaire une fois et restaurez ce fichier en dehors du sous-shell. Je vous préparerai la procédure finale en réponse.
Znik le

Réponses:

156

Premièrement, vous n'augmentez pas le compteur. Changer COUNTER=$((COUNTER))en COUNTER=$((COUNTER + 1))ou COUNTER=$[COUNTER + 1]va l'augmenter.

Deuxièmement, il est plus difficile de rétropropager des variables de sous-shell à l'appelé comme vous le supposez. Les variables d'un sous-shell ne sont pas disponibles en dehors du sous-shell. Ce sont des variables locales au processus enfant.

Une façon de le résoudre consiste à utiliser un fichier temporaire pour stocker la valeur intermédiaire:

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
la source
30
$ [...] est obsolète.
chepner
1
@chepner Avez-vous une référence qui dit $[...]est obsolète? Existe-t-il une solution alternative?
blong
9
$[...]a été utilisé par bashavant a $((...))été adopté par le shell POSIX. Je ne suis pas sûr qu'il ait jamais été formellement obsolète, mais je ne trouve aucune mention à ce sujet dans la bashpage de manuel, et il semble être pris en charge uniquement pour la compatibilité ascendante.
chepner
De plus, $ (...) est préférable à...
Lennart Rolland
7
@blong Voici une question SO sur $ [...] vs $ ((...)) qui discute et fait référence à la dépréciation: stackoverflow.com/questions/2415724/…
Ogre Psalm33
87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

BASH TESTÉ: Centos, SuSE, RH

Jay Stan
la source
1
@kroonwijk, il doit y avoir un espace avant le crochet (pour «délimiter les mots», formellement). Bash ne peut pas voir autrement la fin de l'expression précédente.
EdwardGarson
1
les questions concernaient un certain temps avec un tube, donc là où un sous-shell est créé, votre réponse est correcte mais vous n'utilisez pas de tube, donc il ne répond pas à la question
chrisweb
2
Selon le commentaire de Chepner sur une autre réponse, la $[ ]syntaxe est obsolète. stackoverflow.com/questions/10515964/…
Mark Haferkamp
cela ne résout pas la question principale, la boucle principale est placée sous le sous
Znik le
42
COUNTER=$((COUNTER+1)) 

est une construction assez maladroite dans la programmation moderne.

(( COUNTER++ ))

semble plus «moderne». Vous pouvez aussi utiliser

let COUNTER++

si vous pensez que cela améliore la lisibilité. Parfois, Bash donne trop de façons de faire les choses - la philosophie Perl je suppose - alors que Python "il n'y a qu'une seule bonne façon de le faire" pourrait être plus approprié. C'est une déclaration discutable s'il y en a jamais eu! Quoi qu'il en soit, je suggérerais que le but (dans ce cas) n'est pas seulement d'incrémenter une variable mais (règle générale) d'écrire également du code que quelqu'un d'autre peut comprendre et prendre en charge. La conformité contribue grandement à y parvenir.

HTH

Bill Parker
la source
Cela ne répond pas à la question d'origine, à savoir comment obtenir la valeur mise à jour dans le compteur APRÈS avoir terminé la boucle (sous-processus)
Luis Vazquez
16

Essayez d'utiliser

COUNTER=$((COUNTER+1))

au lieu de

COUNTER=$((COUNTER))
dbf
la source
8
ou tout simplementlet "COUNTER++"
nullpotent
2
Désolé, c'était une faute de frappe. C'est en fait ((COUNTER + 1))
Sparsh Gupta
8
@AaronDigulla: (( COUNTER++ ))(pas de signe dollar)
pause jusqu'à nouvel ordre.
2
Je ne sais pas pourquoi mais je vois un de mes scripts échouer à plusieurs reprises lors de l'utilisation, (( COUNTER++ ))mais lorsque je suis passé, COUNTER=$((COUNTER + 1))cela a fonctionné. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu
Peut-être que votre ligne de hachage fonctionne bash comme / bin / sh au lieu de / bin / bash?
Max
12

Je pense que cet appel awk unique est équivalent à votre grep|grep|awk|awkpipeline: veuillez le tester. Votre dernière commande awk semble ne rien changer du tout.

Le problème avec COUNTER est que la boucle while s'exécute dans un sous-shell, donc toute modification apportée à la variable disparaît lorsque le sous-shell se termine. Vous devez accéder à la valeur de COUNTER dans ce même sous-shell. Ou suivez les conseils de @ DennisWilliamson, utilisez une substitution de processus et évitez complètement le sous-shell.

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
la source
1
Merci, le dernier awk supprimera fondamentalement tout après end = 1 et mettra un nouveau end = 1 à la fin (afin que la prochaine fois, nous puissions supprimer tout ce qui est ajouté après).
Sparsh Gupta
1
@SparshGupta, le awk précédent n'affiche rien après "end = 1".
glenn jackman
Cela améliore très bien le script des questions, mais ne résout pas le problème d'augmentation du compteur à l'intérieur du sous
Znik
12
count=0   
base=1
(( count += base ))
pkm
la source
11

Au lieu d'utiliser un fichier temporaire, vous pouvez éviter de créer un sous-shell autour de la whileboucle en utilisant la substitution de processus.

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

Au fait, vous devriez pouvoir transformer tout cela grep, grep, awk, awk, awken un seul awk.

À partir de Bash 4.2, il existe une lastpipeoption qui

exécute la dernière commande d'un pipeline dans le contexte shell actuel. L'option lastpipe n'a aucun effet si le contrôle des travaux est activé.

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
Suspendu jusqu'à nouvel ordre.
la source
la substitution de processus est excellente si vous souhaitez incrémenter un compteur à l'intérieur de la boucle et l'utiliser à l'extérieur une fois terminé, le problème avec les substitutions de processus est que je n'ai trouvé aucun moyen d'obtenir également le code d'état de la commande exécutée, ce qui est possible lors de l'utilisation d'un tube en utilisant $ {PIPESTATUS [*]}
chrisweb
@chrisweb: J'ai ajouté des informations sur lastpipe. Au fait, vous devriez probablement utiliser "${PIPESTATUS[@]}"(at au lieu de l'astérisque).
Suspendu jusqu'à nouvel ordre.
errata. dans bash (pas en perl comme j'ai été écrit précédemment par erreur) le code de sortie est une table, alors vous pouvez vérifier séparément tous les codes de sortie dans la chaîne de tubes. avant de tester d'abord, votre étape doit être de copier cette table, sinon après la première commande, vous perdrez toutes les valeurs.
Znik le
C'est la solution qui a fonctionné pour moi et sans utiliser de fichier externe pour stocker la valeur de la variable qui est trop piétonne à mon avis.
Luis Vazquez
8

minimaliste

counter=0
((counter++))
echo $counter
geekzspot
la source
Simple :-). Merci @geekzspot
Hussain K
ne fonctionne pas par exemple en question, car il y a sous
coque
3

C'est tout ce que vous devez faire:

$((COUNTER++))

Voici un extrait de Learning the bash Shell , 3e édition, pp.147, 148:

Les expressions arithmétiques bash sont équivalentes à leurs équivalents dans les langages Java et C. [9] La priorité et l'associativité sont les mêmes qu'en C. Le Tableau 6-2 montre les opérateurs arithmétiques pris en charge. Bien que certains d'entre eux soient (ou contiennent) des caractères spéciaux, il n'est pas nécessaire de les échapper par une barre oblique inverse, car ils sont dans la syntaxe $ ((...)).

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

Les opérateurs ++ et - sont utiles lorsque vous souhaitez incrémenter ou décrémenter une valeur de un. [11] Ils fonctionnent de la même manière qu'en Java et C, par exemple, value ++ incrémente la valeur de 1. C'est ce qu'on appelle post-incrément ; il y a aussi un pré-incrément : ++ valeur . La différence devient évidente avec un exemple:

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

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

CE Montijo
la source
C'est la version de ceci dont j'avais besoin, parce que je l'utilisais dans la condition d'une ifdéclaration: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi vrai ou faux, c'est la seule version qui a fonctionné de manière fiable.
LS
L'important dans ce formulaire est que vous pouvez utiliser un incrément en une seule étape. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky
1
@LS: if (( needsComma++ > 0 )); thenouif (( needsComma++ )); then
pause jusqu'à nouvel ordre.
En utilisant "echo $ ((i ++))" dans bash, j'obtiens toujours "/opt/xyz/init.sh: line 29: i: command not found" Qu'est-ce que je fais mal?
mmo
Cela ne répond pas à la question d'obtenir la valeur du compteur en dehors de la boucle.
Luis Vazquez
1

Ceci est un exemple simple

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done
Zwitterion
la source
1
exemple simple, mais non applicable à la question.
Znik
0

Il semble que vous n'ayez pas mis à jour counterle script, utilisezcounter++

yjshen
la source
Toutes mes excuses pour la faute de frappe, j'utilise en fait ((COUNTER + 1)) dans un script qui ne fonctionne pas
Sparsh Gupta
peu importe, il est incrémenté de valeur + 1 ou de valeur ++. Une fois le sous-shell terminé, la valeur du compteur est perdue et revient à la valeur initiale 0 définie au démarrage de ce script.
Znik le
0

Il y avait deux conditions qui ont fait échouer l'expression ((var++))pour moi:

  1. Si je mets bash en mode strict ( set -euo pipefail) et si je commence mon incrément à zéro (0).

  2. Commencer à un (1) est très bien, mais zéro fait que l'incrément renvoie "1" lors de l'évaluation de "++" qui est un échec de code de retour différent de zéro en mode strict.

Je peux utiliser ((var+=1))ou var=$((var+1))échapper à ce comportement

Augustus Hill
la source
0

Le script source a un problème avec le sous-shell. Premier exemple, vous n'avez probablement pas besoin de sous-shell. Mais nous ne savons pas ce qui est caché sous "Encore plus d'action". La réponse la plus populaire a un bogue caché, qui augmentera les E / S, et ne fonctionnera pas avec le sous-shell, car il restaure le routeur à l'intérieur de la boucle.

N'ajoutez pas le signe '\', cela informera l'interpréteur bash de la continuation de la ligne. J'espère que cela vous aidera, vous ou n'importe qui. Mais à mon avis, ce script devrait être entièrement converti en script AWK, ou bien réécrit en python à l'aide de regexp ou de perl, mais la popularité de perl au fil des ans est dégradée. Mieux vaut le faire avec python.

Version corrigée sans sous-shell:

#!/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 avec sous-shell si c'est vraiment nécessaire

#!/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
la source