Perché la mia variabile è locale in un ciclo "while read", ma non in un altro loop apparentemente simile?


25

Perché ottengo valori diversi per $xdai frammenti di seguito?

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

Risposte:


26

La giusta spiegazione è già stata data da jsbillings e geekosaur , ma lasciatemi espandere un po 'su questo.

Nella maggior parte delle shell, incluso bash, ciascun lato di una pipeline viene eseguito in una subshell, quindi qualsiasi cambiamento nello stato interno della shell (come l'impostazione delle variabili) rimane limitato a quel segmento di una pipeline. Le uniche informazioni che è possibile ottenere da una subshell sono ciò che genera (allo standard output e ad altri descrittori di file) e il suo codice di uscita (che è un numero compreso tra 0 e 255). Ad esempio, il frammento seguente stampa 0:

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

In ksh (le varianti derivate dal codice AT&T, non dalle varianti pdksh / mksh) e zsh, l'ultimo elemento in una pipeline viene eseguito nella shell padre. (POSIX consente entrambi i comportamenti.) Quindi lo snippet sopra stampa 2.

Un idioma utile è includere la continuazione del ciclo while (o qualunque cosa tu abbia sul lato destro della pipeline, ma un loop while è effettivamente comune qui) nella pipeline:

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}

1
Grazie Gilles .. Che a = 0; a = 1 | a = 2 fornisce un'immagine molto chiara .. e non solo della localizzazione dello stato interno, ma anche del fatto che una pipeline non ha effettivamente bisogno di inviare nulla attraverso la pipe (tranne il codice di uscita (?) .. Di per sé che è una visione interessante di una pipa ... Sono riuscito a far funzionare la mia sceneggiatura < <(locate -ber ^\.tag$), grazie alla risposta un po 'chiara originale e ai commenti di geekosaur e glenn jackman .. Inizialmente ero in un dilemma sull'accettare la risposta, ma il risultato netto era abbastanza chiaro, specialmente con il commento di follow-up di
jsbillings

mi sembra di essere convogliato in una funzione, quindi ho spostato alcune variabili e test all'interno e ha funzionato benissimo, grazie!
Aquarius Power

8

Stai riscontrando un problema con ambito variabile. Le variabili definite nel ciclo while che si trova sul lato destro della pipe hanno il proprio contesto di ambito locale e le modifiche alla variabile non verranno visualizzate al di fuori del ciclo. Il ciclo while è essenzialmente una subshell che ottiene una COPIA dell'ambiente shell e qualsiasi modifica all'ambiente viene persa alla fine della shell. Vedi questa domanda StackOverflow .

AGGIORNATO : ho trascurato di sottolineare il fatto importante che il ciclo while con la sua stessa subshell era dovuto al fatto che era l'endpoint di una pipe, l'ho aggiornato nella risposta.


@jsbillings .. Va bene, questo spiega gli ultimi due frammenti, ma non spiega il primo, in cui il valore di $ x impostato nel ciclo, viene portato avanti come 55 (oltre l'ambito del ciclo 'while')
Peter.O

5
@ fred.bear: sta eseguendo il whileloop come la coda di una pipeline che lo lancia in una subshell.
Geekosaur,

2
È qui che entra in gioco la sostituzione del processo bash. Invece di blah|blah|while read ..., puoi averewhile read ...; done < <(blah|blah)
glenn jackman il

1
@geekosaur: grazie per aver compilato i dettagli che ho trascurato di includere nella mia risposta.
jsbillings,

1
-1 Mi dispiace ma questa risposta è sbagliata. Spiega come funziona questa roba in molti linguaggi di programmazione ma non nella shell. @Gilles, in basso, ha capito bene.
jpc,

6

Come menzionato in altre risposte , le parti di una pipeline vengono eseguite in subshells, quindi le modifiche apportate non sono visibili alla shell principale.

Se consideriamo solo Bash, ci sono altre due soluzioni alternative oltre alla cmd | { stuff; more stuff; }struttura:

  1. Reindirizzare l'input dalla sostituzione del processo :

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

    L'output del comando in <(...)viene visualizzato come se fosse una pipe denominata.

  2. L' lastpipeopzione, che fa funzionare Bash come ksh, ed esegue l'ultima parte della pipeline nel processo principale della shell. Sebbene funzioni solo se il controllo del lavoro è disabilitato, cioè non in una shell interattiva:

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

    o

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

La sostituzione del processo è ovviamente supportata anche in ksh e zsh. Ma dal momento che eseguono comunque l'ultima parte della pipeline nella shell principale, non è necessario usarlo come soluzione alternativa.


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

può funzionare.

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.