Una variabile modificata all'interno di un ciclo while non viene ricordata


187

Nel seguente programma, se imposto la variabile $foo sul valore 1 all'interno della prima ifistruzione, funziona nel senso che il suo valore viene ricordato dopo l'istruzione if. Tuttavia, quando imposto la stessa variabile sul valore 2 all'interno di un'istruzione ifche si trova all'interno di whileun'istruzione, viene dimenticata dopo il whileciclo. Si comporta come se stessi usando una sorta di copia della variabile $fooall'interno del whileciclo e sto modificando solo quella particolare copia. Ecco un programma di test completo:

#!/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)


L'utilità shellcheck rileva ciò (consultare github.com/koalaman/shellcheck/wiki/SC2030 ); Taglia e incolla il codice sopra in shellcheck.net rilascia questo feedback per la Linea 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Risposte:


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

Il whileciclo viene eseguito in una subshell. Pertanto, eventuali modifiche apportate alla variabile non saranno disponibili una volta terminata la subshell.

Invece puoi usare una stringa qui per riscrivere il ciclo while per essere nel processo principale della shell; echo -e $linesverrà eseguito solo in una subshell:

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

Puoi sbarazzarti del piuttosto brutto echonella stringa qui sopra espandendo immediatamente le sequenze di barre rovesciate durante l'assegnazione lines. La $'...'forma di quotazione può essere utilizzata lì:

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

20
meglio cambiare <<< "$(echo -e "$lines")"in semplice<<< "$lines"
beliy

e se la fonte provenisse tail -finvece del testo fisso?
mt eee,

2
@mteee Puoi usare while read -r line; do echo "LINE: $line"; done < <(tail -f file)(ovviamente il loop non si chiuderà poiché continua ad attendere l'input da tail).
PP,

Sono limitato a / bin / sh - esiste un modo alternativo che funziona nella vecchia shell?
user9645

1
@AvinashYadav Il problema non è realmente correlato a whileloop o forloop; piuttosto l'uso di subshell cioè, in cmd1 | cmd2, cmd2è in una subshell. Quindi, se un forciclo viene eseguito in una subshell, verrà mostrato il comportamento imprevisto / problematico.
PP,

48

AGGIORNAMENTO 2 #

La spiegazione è nella risposta di Blue Moons.

Soluzioni alternative:

Eliminare echo

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

Aggiungi l'eco all'interno del documento Here-is-the-Document

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

Esegui echoin background:

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

Reindirizzare esplicitamente a un file handle (attenzione allo spazio < <!):

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

O semplicemente reindirizzare a stdin:

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

E uno per chepner(eliminare echo):

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

La variabile $linespuò essere convertita in un array senza avviare una nuova shell secondaria. I caratteri \e ndevono essere convertiti in alcuni caratteri (ad es. Un vero carattere di nuova riga) e utilizzare la variabile IFS (Internal Field Separator) per dividere la stringa in elementi dell'array. Questo può essere fatto come:

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

Il risultato è

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")

+1 per here-doc, poiché l' linesunico scopo della variabile sembra essere quello di alimentare il whileciclo.
Chepner,

@chepner: Thx! Ne ho aggiunto un altro, dedicato a te!
Vero

C'è ancora un'altra soluzione data qui :for line in $(echo -e $lines); do ... done
dma_k

@dma_k Grazie per il tuo commento! Questa soluzione produrrebbe 6 righe contenenti una sola parola. La richiesta di OP era diversa ...
TrueY

upvoted. eseguendo l'eco in una subshell all'interno di questo-è, è stata una delle poche soluzioni che ha funzionato in cenere
Hamy,

9

Sei l'utente 742342 ° a chiedere queste FAQ bash. La risposta descrive anche il caso generale delle variabili impostate nelle sottostrutture create dalle pipe:

E4) Se installo l'output di un comando read variable, perché l'output non viene visualizzato $variablequando termina il comando read?

Ciò ha a che fare con la relazione genitore-figlio tra i processi Unix. Colpisce tutti i comandi eseguiti in pipeline, non solo semplici chiamate a read. Ad esempio, il piping dell'output di un comando in un whileloop che chiama ripetutamente readcomporterà lo stesso comportamento.

Ogni elemento di una pipeline, anche una funzione incorporata o shell, viene eseguito in un processo separato, un figlio della shell che esegue la pipeline. Un sottoprocesso non può influire sull'ambiente del suo genitore. Quando il readcomando imposta la variabile sull'input, quella variabile viene impostata solo nella subshell, non nella shell parent. Quando la subshell esce, il valore della variabile viene perso.

Molte pipeline che finiscono con read variablepossono essere convertite in sostituzioni di comandi, che cattureranno l'output di un comando specificato. L'output può quindi essere assegnato a una variabile:

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

può essere convertito in

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

Questo, sfortunatamente, non funziona per dividere il testo tra più variabili, come fa read quando vengono dati argomenti con più variabili. Se è necessario eseguire questa operazione, è possibile utilizzare la sostituzione del comando sopra per leggere l'output in una variabile e tagliare la variabile utilizzando gli operatori di espansione della rimozione del modello bash o utilizzare una variante del seguente approccio.

Dì / usr / local / bin / ipaddr è il seguente script shell:

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

Invece di usare

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

per suddividere l'indirizzo IP del computer locale in ottetti separati, utilizzare

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

Attenzione, tuttavia, che questo cambierà i parametri posizionali della shell. Se ne hai bisogno, dovresti salvarli prima di farlo.

Questo è l'approccio generale: nella maggior parte dei casi non sarà necessario impostare $ IFS su un valore diverso.

Alcune altre alternative fornite dall'utente includono:

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

e, ove sia disponibile la sostituzione del processo,

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

7
Hai dimenticato il problema Family Feud . A volte è molto difficile trovare la stessa combinazione di parole di chiunque abbia scritto la risposta, in modo da non essere invaso da risultati sbagliati, né filtrare la risposta.
Evi1M4chine,

3

Hmmm ... Vorrei quasi giurare che funzionava con la shell Bourne originale, ma non ho accesso a una copia in esecuzione proprio ora per controllare.

Vi è, tuttavia, una soluzione molto banale al problema.

Cambia la prima riga dello script da:

#!/bin/bash

per

#!/bin/ksh

Et voilà! Una lettura alla fine di una pipeline funziona bene, supponendo che tu abbia installato la shell Korn.


1

Uso stderr per archiviare in un ciclo e leggere da esso all'esterno. Qui var i viene inizialmente impostato e letto all'interno del loop come 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

In alternativa, utilizzare il metodo heredoc per ridurre la quantità di codice in una subshell. Si noti che il valore iterativo i può essere letto al di fuori del ciclo while.

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

0

Che ne dici di un metodo molto semplice

    +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.

6
Forse c'è una risposta decente sotto la superficie qui, ma hai macellato la formattazione al punto che è spiacevole provare a leggere.
Mark Amery,

Vuoi dire che il codice originale è piacevole da leggere? (Ho appena seguito: p)
Marcin l'

0

Questa è una domanda interessante e tocca un concetto molto semplice nella shell e nella subshell Bourne. Qui fornisco una soluzione diversa dalle soluzioni precedenti eseguendo una sorta di filtro. Farò un esempio che potrebbe essere utile nella vita reale. Questo è un frammento per verificare che i file scaricati siano conformi a un checksum noto. Il file di checksum è simile al seguente (mostrando solo 3 righe):

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

Lo script della 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

Il genitore e la subshell comunicano tramite il comando echo. Puoi scegliere del testo facile da analizzare per la shell genitore. Questo metodo non rompe il tuo normale modo di pensare, solo che devi fare un po 'di post elaborazione. Puoi usare grep, sed, awk e altro per farlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.