258 votes

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

Dans le programme suivant, si je place la variable $foo à la valeur 1 à l'intérieur du premier if il fonctionne dans le sens où sa valeur est mémorisée après l'instruction if. Cependant, lorsque je donne à la même variable la valeur 2 dans une instruction if qui se trouve à l'intérieur d'un while il est oublié après l'instruction while boucle. C'est comme si j'utilisais une sorte de copie de la variable $foo à l'intérieur de la while et 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)

313voto

KingsIndian Points 26855
echo -e $lines | while read line 
    ...
done

El while est exécutée dans un sous-shell. Ainsi, toute modification apportée à la variable ne sera pas disponible à la sortie du sous-shell.

Au lieu de cela, vous pouvez utiliser un ici la chaîne de réécrire la boucle while pour qu'elle soit dans le processus principal du shell ; seulement echo -e $lines s'exécutera 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 l'aspect plutôt laid echo dans la chaîne ici ci-dessus en développant les séquences de barres obliques inversées immédiatement lors de l'affectation de lines . El $'...' La forme de citation peut y être utilisée :

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

55voto

TrueY Points 2676

UPDATED#2

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

Solutions alternatives :

Éliminez 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 echo en arrière-plan :

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

Rediriger vers un handle de fichier de manière explicite (Attention à l'espace en < < !) :

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 (éliminant echo ):

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

Variable $lines peut être converti en un tableau sans démarrer un nouveau sous-shell. Les caractères \ y n doit être converti en un caractère (par exemple, un vrai caractère de nouvelle ligne) et utiliser la variable IFS (Internal Field Separator) pour diviser la chaîne en éléments de tableau. Cela peut être fait comme suit :

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")

11voto

Jens Points 17702

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

E4) Si je place la sortie d'une commande dans read variable Pourquoi ? la sortie ne s'affiche pas dans $variable lorsque la commande de lecture se termine ?

Cela a à voir avec la relation parent-enfant entre Unix entre les processus Unix. Cela affecte toutes les commandes exécutées dans les pipelines, pas seulement simples appels à read . Par exemple, le passage de la sortie d'une commande dans un while qui appelle de manière répétée read aura pour résultat le même comportement.

Chaque élément d'un pipeline, même un buildin ou une fonction shell, s'exécute dans un processus séparé, un enfant du shell qui exécute le pipeline. pipeline. Un sous-processus ne peut pas affecter l'environnement de son parent. Lorsque le read définit la variable à l'entrée, que variable est définie uniquement dans le sous-shell, et non 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 variable peut être converti en substitutions de commandes, qui captureront la sortie d'une commande spécifique. d'une commande spécifiée. La sortie peut alors être assigné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 pas, malheureusement, pour diviser le texte entre variables multiples, comme le fait read lorsqu'on lui donne plusieurs variables multiples. Si vous devez le faire, vous pouvez soit utiliser la commande ci-dessus pour lire la sortie dans une variable et découper la variable en utilisant les opérateurs d'expansion bash de suppression de motif ou utiliser une variante de l'approche suivante approche.

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

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

Au lieu d'utiliser

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

pour décomposer 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 la position de l'interpréteur de commandes. de l'interpréteur de commandes. Si vous en avez besoin, vous devez les sauvegarder avant d'effectuer cette opération. cette opération.

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

Parmi les autres solutions proposées par les utilisateurs, citons :

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

et, lorsque la substitution de processus est possible,

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

6voto

Jonathan Quist Points 71

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

Il existe toutefois une solution très simple à ce problème.

Changez 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é.

2voto

bugdot Points 21

J'utilise stderr pour stocker à l'intérieur d'une boucle, et le lire à l'extérieur. Ici, var i est initialement défini et lu à l'intérieur de 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

Vous pouvez aussi utiliser la méthode heredoc pour réduire la quantité de code dans un sous-shell. Notez que la valeur itérative i 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X