bash: la variable perd sa valeur à la fin de la boucle while

36

J'ai un problème dans l'un de mes scripts shell. A demandé à quelques collègues, mais ils secouent tous la tête (après quelques égratignures), alors je suis venu ici pour une réponse.

Selon ma compréhension, le script suivant devrait afficher "Le nombre est 5" à la dernière ligne. Sauf que ce n'est pas le cas. Il imprime "Le compte est 0". Si le "while read" est remplacé par un autre type de boucle, cela fonctionne très bien. Voici le script:

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 | en lisant;
faire
  laissez CNT ++;
  echo "Compter jusqu'à $ CNT"
terminé 
echo "Count is $ CNT"

Pourquoi cela se produit-il et comment puis-je l'éviter? J'ai déjà essayé cela dans Lenny et Squeeze de Debian, le même résultat (c'est-à-dire bash 3.2.39 et bash 4.1.5. J'admets pleinement que je ne suis pas un assistant de script shell, alors tout pointeur serait apprécié.

Wolfgangsz
la source

Réponses:

30

Voir l'argument # 24 de la FAQ @ Bash: "Je définis des variables dans une boucle. Pourquoi elles disparaissent soudainement à la fin de la boucle? Pourquoi ne puis-je pas canaliser les données à lire?" (plus récemment archivé ici ).

Résumé: Ceci n'est supporté qu'à partir de la version 4.2 et plus. Si vous utilisez bash, vous devez utiliser différentes méthodes, telles que la substitution de commandes, au lieu d’un tube.

Ignacio Vazquez-Abrams
la source
Vous obtenez le bonus, car votre réponse m'a fourni le plus large éventail d'options.
Wolfgangsz
5
Le lien est mort. C'est pourquoi les réponses avec lien uniquement sont mauvaises. Résumez au moins la réponse ici.
Rudolfbyker
bon dieu, encore une fois où ksh est simplement tellement meilleur ... pourquoi, pourquoi tout le monde s'est-il rassemblé autour de Bash
Florian Heigl
@ FlorianHeigl: prétendez-vous que ksh est le seul vrai shell?
Ignacio Vazquez-Abrams
@ IgnacioVazquez-Abrams non, mais je prétends que la gestion de la boucle while en bash est un horriblement PITA. Le traitement des boucles était le plus long coureur qui l’empêchait d’accepter la fonctionnalité de 1993. Les autres choses sont la gestion de getopt où le gestionnaire intégré (également de 1993) était simple et capable, quelque chose que vous ne pouvez toujours pas obtenir à moins d'utiliser docopt. Je prétends que bash se tient derrière la courbe depuis plus de 20 ans avec insistance et que le temps consacré à THIS Thing HERE ou à des millions d'utilisations de mauvaise qualité est sans commune mesure - accepté uniquement car la plupart des gens ne le sauront jamais.
Florian Heigl
30

C'est une sorte d'erreur "commune". Les pipes créent des SubShells, ainsi, il while reads’exécute sur un shell différent de celui de votre script, ce qui fait que votre CNTvariable ne change jamais (seulement celle qui se trouve dans le sous-shell du pipe).

Regroupez le dernier echoavec le sous-shell whilepour le réparer (il existe de nombreuses autres façons de le réparer, en voici un. Les réponses d'Iain et d'Ignacio en ont d'autres.)

CNT=0

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

Longue explication:

  1. Vous déclarez CNTsur votre script la valeur 0;
  2. Un SubShell est démarré sur le |à while read;
  3. Votre $CNTvariable est exportée vers le SubShell avec la valeur 0;
  4. Le SubShell compte et augmente la CNTvaleur à 5;
  5. SubShell se termine, les variables et les valeurs sont détruites (elles ne retournent pas au processus / script d'appel).
  6. Vous echovotre CNTvaleur initiale de 0.
coredump
la source
2
Le premier script shell que j'ai écrit m'a donné les mêmes problèmes, m'a cogné la tête contre le mur pendant un certain temps avant de découvrir que ces tuyaux généraient des obus supplémentaires. Toute variable avec laquelle vous jouez dans un tuyau disparaît dès que le tuyau se termine - ce qui signifie que si vous voulez vraiment faire quelque chose avec une variable en dehors du tuyau dans lequel il a été utilisé, vous devrez maintenir l'état par quelque chose de génial comme un fichier temporaire.
photoionisé
Excellente réponse, malheureusement, je ne peux donner qu'un seul bonus d'acceptation. Désolé.
Wolfgangsz
10

Cela marche

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 prend en charge GoFundMonica
la source
J'aime ça, la façon intelligente parce que vous savez où se trouvent les données nécessaires et que vous n'avez besoin que de les récupérer. Si vous ne connaissez pas les solutions de haute compétence, vous pouvez toujours "lire un fichier" hahahha. +1 pour vous.
m3nda
1
Si vous lisez ceci, sachez que la solution fournie par Iain ne fonctionne que lorsque votre script appelle explicitement bash, en ayant la première ligne: #! / Bin / bash et que: #! / Bin / sh ne fonctionnera pas.
Roadowl
1
Exemple intéressant, d' abord je jamais vu où Useless utilisation de Cat effectivement le code empêché de travailler . En passant, @Roadowl, le seul basisme ici est la ligne let CNT++qui devrait plutôt être CNT="$((CNT+1))"de faire appel à un développement arithmétique conforme à POSIX . Le reste est déjà portable.
Wildcard
6

Essayez plutôt de transmettre les données dans un sous-shell, comme s'il s'agissait d'un fichier avant la boucle while. Ceci est similaire à la solution de lain, mais suppose que vous ne voulez pas de fichier intermittent:

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
la source