Une variable modifiée à l'intérieur d'une boucle while n'est pas mémorisée

187

Dans le programme suivant, si je règle la variable $foosur la valeur 1 dans la première ifinstruction, cela fonctionne dans le sens où sa valeur est mémorisée après l'instruction if. Cependant, lorsque je mets la même variable à la valeur 2 à l'intérieur d'un ifqui se trouve à l'intérieur d'une whileinstruction, elle est oubliée après la whileboucle. Cela se comporte comme si j'utilisais une sorte de copie de la variable $fooà l'intérieur de la whileboucle et que je ne modifie que cette copie particulière. Voici un programme de test complet:

#!/bin/bash

set -e
set -u 
foo=0
bar="hello"  
if [[ "$bar" == "hello" ]]
then
    foo=1
    echo "Setting \$foo to 1: $foo"
fi

echo "Variable \$foo after if statement: $foo"   
lines="first line\nsecond line\nthird line" 
echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2
    echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done

echo "Variable \$foo after while loop: $foo"

# Output:
# $ ./testbash.sh
# Setting $foo to 1: 1
# Variable $foo after if statement: 1
# Value of $foo in while loop body: 1
# Variable $foo updated to 2 inside if inside while loop
# Value of $foo in while loop body: 2
# Value of $foo in while loop body: 2
# Variable $foo after while loop: 1

# bash --version
# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)
Eric Lilja
la source
L'utilitaire shellcheck détecte cela (voir github.com/koalaman/shellcheck/wiki/SC2030 ); Couper et coller le code ci-dessus dans shellcheck.net émet ce commentaire pour la ligne 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Réponses:

244
echo -e $lines | while read line 
    ...
done

La whileboucle est exécutée dans un sous-shell. Ainsi, toutes les modifications que vous apportez à la variable ne seront pas disponibles une fois le sous-shell terminé.

Au lieu de cela, vous pouvez utiliser une chaîne here pour réécrire la boucle while pour qu'elle soit dans le processus principal du shell; uniquement echo -e $linesfonctionnera dans un sous-shell:

while read line
do
    if [[ "$line" == "second line" ]]
    then
        foo=2
        echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done <<< "$(echo -e "$lines")"

Vous pouvez vous débarrasser de ce qui est plutôt laid echodans la chaîne ci-dessus en développant les séquences de barre oblique inverse immédiatement lors de l'assignation lines. Le $'...'formulaire de devis peut y être utilisé:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"
PP
la source
20
meilleur changement <<< "$(echo -e "$lines")"pour simple<<< "$lines"
beliy
et si la source provenait tail -fplutôt que d'un texte fixe?
mt eee
2
@mteee Vous pouvez utiliser while read -r line; do echo "LINE: $line"; done < <(tail -f file)(évidemment la boucle ne se terminera pas car elle continue d'attendre l'entrée de tail).
PP
Je suis limité à / bin / sh - existe-t-il une autre manière de fonctionner dans l'ancien shell?
user9645
1
@AvinashYadav Le problème n'est pas vraiment lié à la whileboucle ou à la forboucle; plutôt l'utilisation du sous-shell c'est-à-dire, dans cmd1 | cmd2, cmd2est dans un sous-shell. Donc, si une forboucle est exécutée dans un sous-shell, le comportement inattendu / problématique sera affiché.
PP
48

MISE À JOUR # 2

L'explication est dans la réponse de Blue Moons.

Solutions alternatives:

Éliminer echo

while read line; do
...
done <<EOT
first line
second line
third line
EOT

Ajoutez l'écho à l'intérieur du document here-is-the-document

while read line; do
...
done <<EOT
$(echo -e $lines)
EOT

Exécuter echoen arrière-plan:

coproc echo -e $lines
while read -u ${COPROC[0]} line; do 
...
done

Rediriger explicitement vers un descripteur de fichier (attention à l'espace dans < <!):

exec 3< <(echo -e  $lines)
while read -u 3 line; do
...
done

Ou simplement rediriger vers le stdin:

while read line; do
...
done < <(echo -e  $lines)

Et un pour chepner(éliminer echo):

arr=("first line" "second line" "third line");
for((i=0;i<${#arr[*]};++i)) { line=${arr[i]}; 
...
}

La variable $linespeut être convertie en tableau sans démarrer un nouveau sous-shell. Les caractères \et ndoivent être convertis en un certain caractère (par exemple un caractère de nouvelle ligne réel) et utiliser la variable IFS (Internal Field Separator) pour diviser la chaîne en éléments de tableau. Cela peut être fait comme:

lines="first line\nsecond line\nthird line"
echo "$lines"
OIFS="$IFS"
IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion
IFS="$OIFS"
echo "${arr[@]}", Length: ${#arr[*]}
set|grep ^arr

Le résultat est

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
VraiY
la source
+1 pour l'ici-doc, puisque le linesseul but de la variable semble être d'alimenter la whileboucle.
chepner
@chepner: Merci! J'en ai ajouté un autre, dédié à Toi!
TrueY
Il y a encore une autre solution donnée ici :for line in $(echo -e $lines); do ... done
dma_k
@dma_k Merci pour votre commentaire! Cette solution résulterait en 6 lignes contenant un seul mot. La demande d'OP était différente ...
TrueY
voté. exécuter l'écho dans un sous-shell à l'intérieur de here-is, était l'une des rares solutions qui fonctionnait dans ash
Hamy
9

Vous êtes le 742342nd utilisateur à poser cette FAQ bash. La réponse décrit également le cas général des variables définies dans des sous-shell créés par des tubes:

E4) Si je dirige la sortie d'une commande vers read variable, pourquoi la sortie n'apparaît-elle pas $variablelorsque la commande de lecture se termine?

Cela a à voir avec la relation parent-enfant entre les processus Unix. Il affecte toutes les commandes exécutées dans les pipelines, pas seulement les simples appels à read. Par exemple, diriger la sortie d'une commande dans une whileboucle qui appelle à plusieurs reprises readentraînera le même comportement.

Chaque élément d'un pipeline, même une fonction intégrée ou une fonction shell, s'exécute dans un processus séparé, un enfant du shell exécutant le pipeline. Un sous-processus ne peut pas affecter l'environnement de son parent. Lorsque la readcommande définit la variable sur l'entrée, cette variable est définie uniquement dans le sous-shell, pas dans le shell parent. Lorsque le sous-shell se termine, la valeur de la variable est perdue.

De nombreux pipelines qui se terminent par read variablepeuvent être convertis en substitutions de commandes, qui captureront la sortie d'une commande spécifiée. La sortie peut alors être affectée à une variable:

grep ^gnu /usr/lib/news/active | wc -l | read ngroup

peut être converti en

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)

Cela ne fonctionne malheureusement pas pour diviser le texte entre plusieurs variables, comme le fait la lecture lorsqu'on lui donne plusieurs arguments de variable. Si vous devez faire cela, vous pouvez soit utiliser la substitution de commande ci-dessus pour lire la sortie dans une variable et découper la variable à l'aide des opérateurs d'expansion de suppression de modèle bash, soit utiliser une variante de l'approche suivante.

Dites que / usr / local / bin / ipaddr est le script shell suivant:

#! /bin/sh
host `hostname` | awk '/address/ {print $NF}'

À la place d'utiliser

/usr/local/bin/ipaddr | read A B C D

pour diviser l'adresse IP de la machine locale en octets séparés, utilisez

OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="$1" B="$2" C="$3" D="$4"

Attention cependant, cela modifiera les paramètres de position de la coque. Si vous en avez besoin, vous devez les enregistrer avant de faire cela.

C'est l'approche générale - dans la plupart des cas, vous n'aurez pas besoin de définir $ IFS sur une valeur différente.

Certaines autres alternatives fournies par l'utilisateur incluent:

read A B C D << HERE
    $(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE

et, lorsque la substitution de processus est disponible,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
Jens
la source
7
Vous avez oublié le problème de Family Feud . Parfois, il est très difficile de trouver la même combinaison de mots que celui qui a écrit la réponse, de sorte que vous ne soyez ni submergé de mauvais résultats, ni filtrer ladite réponse.
Evi1M4chine
3

Hmmm ... Je jurerais presque que cela a fonctionné pour le shell Bourne original, mais je n'ai pas accès à une copie en cours d'exécution pour le moment.

Il existe cependant une solution de contournement très simple au problème.

Modifiez la première ligne du script de:

#!/bin/bash

à

#!/bin/ksh

Et voilà! Une lecture à la fin d'un pipeline fonctionne très bien, en supposant que le shell Korn soit installé.

Jonathan Quist
la source
1

J'utilise stderr pour stocker dans une boucle et en lire à l'extérieur. Ici, var i est initialement défini et lu dans la boucle comme 1.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value

f=/tmp/file1
g=/tmp/file2
i=1
cat $f $g | \
while read -r s;
do
  echo $s > /dev/null;  # some work
  echo $i > 2
  let i++
done;
read -r i < 2
echo $i

Ou utilisez la méthode heredoc pour réduire la quantité de code dans un sous-shell. Notez que la valeur i itérative peut être lue en dehors de la boucle while.

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i
bugdot
la source
0

Que diriez-vous d'une méthode très simple

    +call your while loop in a function 
     - set your value inside (nonsense, but shows the example)
     - return your value inside 
    +capture your value outside
    +set outside
    +display outside


    #!/bin/bash
    # set -e
    # set -u
    # No idea why you need this, not using here

    foo=0
    bar="hello"

    if [[ "$bar" == "hello" ]]
    then
        foo=1
        echo "Setting  \$foo to $foo"
    fi

    echo "Variable \$foo after if statement: $foo"

    lines="first line\nsecond line\nthird line"

    function my_while_loop
    {

    echo -e $lines | while read line
    do
        if [[ "$line" == "second line" ]]
        then
        foo=2; return 2;
        echo "Variable \$foo updated to $foo inside if inside while loop"
        fi

        echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2;          
    echo "Variable \$foo updated to $foo inside if inside while loop"
    return 2;
    fi

    # Code below won't be executed since we returned from function in 'if' statement
    # We aready reported the $foo var beint set to 2 anyway
    echo "Value of \$foo in while loop body: $foo"

done
}

    my_while_loop; foo="$?"

    echo "Variable \$foo after while loop: $foo"


    Output:
    Setting  $foo 1
    Variable $foo after if statement: 1
    Value of $foo in while loop body: 1
    Variable $foo after while loop: 2

    bash --version

    GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
    Copyright (C) 2007 Free Software Foundation, Inc.
Marcin
la source
6
Peut-être qu'il y a une réponse décente sous la surface ici, mais vous avez massacré le formatage au point qu'il est désagréable d'essayer de lire.
Mark Amery
Vous voulez dire que le code original est agréable à lire? (Je viens de suivre: p)
Marcin
0

C'est une question intéressante et touche à un concept très basique dans Bourne shell et subshell. Ici, je propose une solution différente des solutions précédentes en effectuant une sorte de filtrage. Je vais donner un exemple qui peut être utile dans la vraie vie. Il s'agit d'un fragment permettant de vérifier que les fichiers téléchargés sont conformes à une somme de contrôle connue. Le fichier de somme de contrôle ressemble à ce qui suit (avec seulement 3 lignes):

49174 36326 dna_align_feature.txt.gz
54757     1 dna.txt.gz
55409  9971 exon_transcript.txt.gz

Le script shell:

#!/bin/sh

.....

failcnt=0 # this variable is only valid in the parent shell
#variable xx captures all the outputs from the while loop
xx=$(cat ${checkfile} | while read -r line; do
    num1=$(echo $line | awk '{print $1}')
    num2=$(echo $line | awk '{print $2}')
    fname=$(echo $line | awk '{print $3}')
    if [ -f "$fname" ]; then
        res=$(sum $fname)
        filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
        if [ "$filegood" = "FALSE" ]; then
            failcnt=$(expr $failcnt + 1) # only in subshell
            echo "$fname BAD $failcnt"
        fi
    fi
done | tail -1) # I am only interested in the final result
# you can capture a whole bunch of texts and do further filtering
failcnt=${xx#* BAD } # I am only interested in the number
# this variable is in the parent shell
echo failcnt $failcnt
if [ $failcnt -gt 0 ]; then
    echo $failcnt files failed
else
    echo download successful
fi

Le parent et le sous-shell communiquent via la commande echo. Vous pouvez choisir du texte facile à analyser pour le shell parent. Cette méthode ne rompt pas votre façon de penser habituelle, mais simplement que vous devez effectuer un post-traitement. Vous pouvez utiliser grep, sed, awk, etc. pour ce faire.

Kemin Zhou
la source