Pourquoi ma variable est-elle locale dans une boucle "en lecture", mais pas dans une autre boucle apparemment similaire?

25

Pourquoi est-ce que j'obtiens des valeurs différentes pour $xles extraits ci-dessous?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?
Peter.O
la source
Article similaire sur Stack Overflow: une variable modifiée dans une boucle while n'est pas mémorisée .
codeforester

Réponses:

26

La bonne explication a déjà été donnée par jsbillings et geekosaur , mais permettez-moi de développer un peu.

Dans la plupart des shells, y compris bash, chaque côté d'un pipeline s'exécute dans un sous-shell, de sorte que toute modification de l'état interne du shell (comme la définition de variables) reste limitée à ce segment d'un pipeline. Les seules informations que vous pouvez obtenir d'un sous-shell sont ce qu'il génère (vers la sortie standard et d'autres descripteurs de fichiers) et son code de sortie (qui est un nombre compris entre 0 et 255). Par exemple, l'extrait de code suivant affiche 0:

a=0; a=1 | a=2; echo $a

Dans ksh (les variantes dérivées du code AT&T, pas les variantes pdksh / mksh) et zsh, le dernier élément d'un pipeline est exécuté dans le shell parent. (POSIX autorise les deux comportements.) L'extrait ci-dessus s'imprime donc 2.

Un idiome utile consiste à inclure la continuation de la boucle while (ou tout ce que vous avez sur le côté droit du pipeline, mais une boucle while est en fait courante ici) dans le pipeline:

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}
Gilles 'SO- arrête d'être méchant'
la source
1
Merci Gilles .. Que a = 0; a = 1 | a = 2 donne une image très claire .. et non seulement de la localisation de l'état interne, mais aussi qu'un pipeline n'a en fait pas besoin d'envoyer quoi que ce soit à travers le tuyau (autre que le code de sortie (?) .. En soi, est un aperçu intéressant d'une pipe ... J'ai réussi à faire fonctionner mon script < <(locate -ber ^\.tag$), grâce à la réponse originale peu claire et aux déclarations de geekosaur et de glenn jackman .. J'étais initialement dans un dilemme d'accepter la réponse, mais le résultat net était assez clair, en particulier avec le commentaire de suivi de jsbillings :)
Peter.O
j'ai l'impression d'avoir intégré une fonction, j'ai donc déplacé des variables et des tests à l'intérieur et cela a très bien fonctionné, merci!
Aquarius Power
8

Vous rencontrez un problème de portée variable. Les variables définies dans la boucle while qui se trouve sur le côté droit du canal ont leur propre contexte de portée locale, et les modifications apportées à la variable ne seront pas vues en dehors de la boucle. La boucle while est essentiellement un sous-shell qui obtient une copie de l'environnement du shell, et toutes les modifications apportées à l'environnement sont perdues à la fin du shell. Voir cette question StackOverflow .

MISE À JOUR : J'ai négligé de souligner le fait important que la boucle while avec son propre sous-shell était due au fait qu'il s'agissait du point de terminaison d'un tuyau, je l'ai mis à jour dans la réponse.

jsbillings
la source
@jsbillings .. D'accord, cela explique les deux derniers extraits, mais cela n'explique pas le premier, où la valeur de $ x définie dans la boucle est maintenue à 55 (au-delà de la portée de la boucle `` while '')
Peter.O
5
@ fred.bear: il exécute la whileboucle en tant qu'extrémité arrière d'un pipeline qui la jette dans un sous-shell.
geekosaur
2
C'est là que la substitution de processus bash entre en jeu. Au lieu de blah|blah|while read ..., vous pouvez avoirwhile read ...; done < <(blah|blah)
glenn jackman
1
@geekosaur: merci d'avoir rempli les détails que j'ai négligé d'inclure dans ma réponse.
jsbillings
1
-1 Désolé mais cette réponse est tout simplement fausse. Il explique comment cela fonctionne dans de nombreux langages de programmation mais pas dans le shell. @ Gilles, ci-dessous, a bien compris.
jpc
6

Comme mentionné dans d'autres réponses , les parties d'un pipeline s'exécutent en sous-coquilles, donc les modifications qui y sont apportées ne sont pas visibles pour le shell principal.

Si nous considérons uniquement Bash, il existe deux autres solutions de contournement en plus de la cmd | { stuff; more stuff; }structure:

  1. Rediriger l'entrée de la substitution de processus :

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    La sortie de la commande dans <(...)est faite pour apparaître comme s'il s'agissait d'un canal nommé.

  2. L' lastpipeoption, qui fait fonctionner Bash comme ksh, et exécute la dernière partie du pipeline dans le processus shell principal. Bien que cela ne fonctionne que si le contrôle des travaux est désactivé, c'est-à-dire pas dans un shell interactif:

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    ou

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

La substitution de processus est bien sûr également prise en charge dans ksh et zsh. Mais puisqu'ils exécutent de toute façon la dernière partie du pipeline dans le shell principal, son utilisation comme solution de contournement n'est pas vraiment nécessaire.

ilkkachu
la source
0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

ça peut marcher.

GPS
la source