Por que minha variável local é em um loop 'enquanto lê', mas não em outro loop aparentemente semelhante?

25

Por que obtenho valores diferentes para $xos snippets abaixo?

#!/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?
Peter.O
fonte
Postagem semelhante no Stack Overflow: uma variável modificada dentro de um loop while não é lembrada .
Codeforester

Respostas:

26

A explicação correta já foi dada por jsbillings e geekosaur , mas deixe-me expandir um pouco.

Na maioria dos shells, incluindo o bash, cada lado de um pipeline é executado em um subshell, portanto, qualquer alteração no estado interno do shell (como definir variáveis) permanece confinada ao segmento de um pipeline. A única informação que você pode obter de um subshell é o que ele gera (para saída padrão e outros descritores de arquivo) e seu código de saída (que é um número entre 0 e 255). Por exemplo, o seguinte snippet imprime 0:

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

No ksh (as variantes derivadas do código AT&T, não nas variantes pdksh / mksh) e no zsh, o último item de um pipeline é executado no shell pai. (O POSIX permite os dois comportamentos.) Portanto, o trecho acima imprime 2.

Um idioma útil é incluir a continuação do loop while (ou o que você tiver no lado direito do pipeline, mas um loop while é realmente comum aqui) no pipeline:

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}
Gilles 'SO- parar de ser mau'
fonte
11
Obrigado Gilles .. Que a = 0; a = 1 | a = 2 fornece uma imagem muito clara .. e não apenas da localização do estado interno, mas também que um pipeline não precisa realmente enviar nada através do canal (exceto o código de saída (?) .. é uma visão interessante de um cano ... Eu consegui rodar meu script < <(locate -ber ^\.tag$), graças à resposta pouco clara original e aos avisos de geekosaur e glenn jackman .. Eu estava inicialmente em um dilema sobre aceitar a resposta, mas o resultado foi foi muito claro, especialmente com jsbillings acompanhamento comentário :)
Peter.O
parece que eu canalizei para uma função, então mudei algumas variáveis ​​e testes para dentro dela e funcionou muito bem, thx!
Aquarius Power
8

Você está enfrentando um problema de escopo variável. As variáveis ​​definidas no loop while, que estão no lado direito do canal, têm seu próprio contexto de escopo local e as alterações na variável não serão vistas fora do loop. O loop while é essencialmente um subshell que obtém uma cópia do ambiente do shell, e quaisquer alterações no ambiente são perdidas no final do shell. Veja esta pergunta StackOverflow .

ATUALIZADO : Esqueci de salientar o fato importante de que o loop while com seu próprio subshell se deve ao fato de ser o ponto final de um canal, atualizei isso na resposta.

jsbillings
fonte
@jsbillings .. Ok, isso explica os dois últimos trechos, mas não explica o primeiro, onde o valor de $ x definido no loop é transportado como 55 (além do escopo do loop 'while')
Peter.O 23/03
5
@ fred.bear: está executando o whileloop como o final de um pipeline que o lança em um subshell.
Geekosaur #
2
É aqui que a substituição do processo bash entra em cena. Em vez de blah|blah|while read ..., você pode terwhile read ...; done < <(blah|blah)
glenn jackman 23/03
11
@geekosaur: obrigado por preencher os detalhes que deixei de incluir na minha resposta.
jsbillings
11
-1 Desculpe, mas esta resposta está errada. Ele explica como essas coisas funcionam em muitas linguagens de programação, mas não no shell. @ Gilles, abaixo, acertou.
precisa
6

Como mencionado em outras respostas , as partes de um pipeline são executadas em subcascas, portanto as modificações feitas lá não são visíveis para o shell principal.

Se considerarmos apenas o Bash, há duas outras soluções alternativas além da cmd | { stuff; more stuff; }estrutura:

  1. Redirecione a entrada da substituição do processo :

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

    A saída do comando in <(...)é feita para parecer como se fosse um pipe nomeado.

  2. A lastpipeopção, que faz o Bash funcionar como ksh, e executa a última parte do pipeline no processo principal do shell. Embora só funcione se o controle de trabalho estiver desabilitado, ou seja, não em um shell interativo:

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

    ou

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

É claro que a substituição de processos também é suportada no ksh e no zsh. Mas como eles executam a última parte do pipeline no shell principal de qualquer maneira, usá-lo como solução alternativa não é realmente necessário.

ilkkachu
fonte
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

isso pode funcionar.

GPS
fonte