web-dev-qa-db-fra.com

Une variable modifiée dans une boucle while n'est pas mémorisée

Dans le programme suivant, si je mets la variable $foo à la valeur 1 dans la première instruction if, 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'une if qui est à l'intérieur d'une instruction while, elle est oubliée après la boucle while. C'est comme si j'utilisais une sorte de copie de la variable $foo à l'intérieur de la boucle while et que je ne modifiais que cette copie. 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)
158
Eric Lilja
echo -e $lines | while read line 
    ...
done

La boucle while est exécutée dans un sous-shell. Ainsi, toute modification apportée à la variable ne sera plus disponible une fois le sous-shell terminé.

Au lieu de cela, vous pouvez utiliser une chaîne ici) pour réécrire la boucle while afin qu'elle soit dans le processus principal du shell; seul echo -e $lines sera exécuté 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 la echo plutôt moche de la chaîne here ci-dessus en développant immédiatement les séquences de barres obliques inverses lors de l'attribution de lines. La forme de citation $'...' peut être utilisée ici:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"
201
P.P.

MISE À JOUR N ° 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

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

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

Exécutez echo en arrière-plan:

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

Rediriger explicitement vers un descripteur de fichier (notez 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 (en éliminant echo):

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

La variable $lines peut être convertie en tableau sans démarrer un nouveau sous-shell. Les caractères \ et n doivent être convertis en un caractère (par exemple, un nouveau caractère de ligne réel) et utiliser la variable IFS (séparateur de champs internes) pour scinder 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")
45
TrueY

Vous êtes le 742342ème utilisateur à demander ceci FAQ de bash. La réponse décrit également le cas général des variables définies dans des sous-réservoirs créés par des tubes:

E4) Si je transfère la sortie d'une commande dans read variable, pourquoi la sortie ne s'affiche-t-elle pas dans $variable lorsque la commande de lecture se termine?

Cela concerne la relation parent-enfant entre les processus Unix. Cela affecte toutes les commandes exécutées dans les pipelines, pas seulement les simples appels à read. Par exemple, canaliser la sortie d'une commande dans une boucle while qui appelle de manière répétée read entraîne le même comportement.

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

De nombreux pipelines se terminant par read variable peuvent être convertis en substitutions de commande, qui captureront le résultat d'une commande spécifiée. La sortie peut ensuite ê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)

Malheureusement, cela ne permet pas de scinder le texte en plusieurs variables, contrairement à read lorsque l’on donne plusieurs arguments de variable. Si vous avez besoin de le faire, vous pouvez utiliser la substitution de commande ci-dessus pour lire le résultat dans une variable et découper la variable à l'aide des opérateurs d'expansion de la suppression du modèle bash ou utiliser une variante de l'approche suivante.

Dites/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 toutefois, cela modifiera les paramètres de position du shell. Si vous en avez besoin, vous devriez les enregistrer avant de le faire.

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.

Parmi les autres solutions proposées par l'utilisateur, citons:

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))
9
Jens

Hmmm ... Je jurerais presque que cela fonctionne pour le Bourne Shell original, mais je n'ai pas accès à une copie en cours d'exécution pour l'instant.

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

Modifiez la première ligne du script à partir de:

#!/bin/bash

à

#!/bin/ksh

Et voilà! Une lecture à la fin d'un pipeline fonctionne très bien si Korn Shell est installé.

1
Jonathan Quist

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.
0
Marcin

C'est une question intéressante et touche à un concept très basique dans Bourne Shell et sous-shell. 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 vie réelle. Ceci est 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 se présente comme suit (ne montre que 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 avec votre façon de penser habituelle, mais vous oblige à faire du post-traitement. Vous pouvez utiliser grep, sed, awk et plus encore pour le faire.

0
Kemin Zhou