Uma variável modificada dentro de um loop while não é lembrada

No programa a seguir, se eu definir a variável $foo para o valor 1 dentro da primeira instrução if , ela funcionará no sentido de que seu valor é lembrado após a instrução if. No entanto, quando eu definir a mesma variável para o valor 2 dentro de um if que está dentro de uma instrução while , é esquecido após o loop while. Está se comportando como se eu estivesse usando algum tipo de cópia da variável $foo dentro do loop while e estou modificando apenas aquela cópia em particular. Aqui está um programa de teste 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) 

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

    O loop while é executado em um subshell. Portanto, quaisquer alterações feitas na variável não estarão disponíveis depois que a sub-ssh sair.

    Em vez disso, você pode usar uma string aqui para rewrite o loop while para estar no processo shell principal; Apenas as echo -e $lines serão executadas em um 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")" 

    ATUALIZADO # 2

    A explicação está na resposta da Blue Moons.

    Soluções alternativas:

    Elimine o echo

     while read line; do ... done <  

    Adicione o eco dentro do aqui-é-o-documento

     while read line; do ... done <  

    Execute o echo no fundo:

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

    Redirecionar para um identificador de arquivo explicitamente (ocupe o espaço em < < !):

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

    Ou apenas redirecione para o stdin :

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

    E um para o chepner (eliminando o echo ):

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

    Variáveis $lines podem ser convertidas em um array sem iniciar um novo sub-shell. Os caracteres \ n precisam ser convertidos em algum caractere (por exemplo, um caractere real de nova linha) e usar a variável IFS (Internal Field Separator) para dividir a string em elementos da matriz. Isso pode ser feito como:

     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 

    Resultado é

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

    Você é o usuário do 742342nd para fazer este FAQ bash. A resposta também descreve o caso geral de variables ​​definidas em subshells criadas por pipes:

    E4) Se eu canalizar a saída de um comando para a read variable , por que a saída não aparece na $variable quando o comando read termina?

    Isso tem a ver com o relacionamento pai-filho entre processos Unix. Afeta todos os comandos executados em pipelines, não apenas chamadas simples para read . Por exemplo, canalizar a saída de um comando em um loop while que chama repetidamente a read resultará no mesmo comportamento.

    Cada elemento de um pipeline, até mesmo uma function integrada ou shell, é executado em um processo separado, um filho do shell executando o pipeline. Um subprocess não pode afetar o ambiente do pai. Quando o comando read configura a variável para a input, essa variável é definida apenas no subshell, não no shell pai. Quando o subshell sai, o valor da variável é perdido.

    Muitos pipelines que terminam com a read variable podem ser convertidos em substituições de comandos, que capturarão a saída de um comando especificado. A saída pode então ser atribuída a uma variável:

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

    pode ser convertido em

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

    Infelizmente, isso não funciona para dividir o texto entre várias variables, como ocorre quando vários argumentos variables ​​são fornecidos. Se você precisar fazer isso, você pode usar a substituição de comando acima para ler a saída em uma variável e dividir a variável usando os operadores de expansão de remoção do padrão bash ou usar alguma variante da abordagem a seguir.

    Diga / usr / local / bin / ipaddr é o seguinte script de shell:

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

    Ao invés de usar

     /usr/local/bin/ipaddr | read ABCD 

    para quebrar o endereço IP da máquina local em octetos separados, use

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

    Cuidado, no entanto, que isso alterará os parâmetros posicionais do shell. Se você precisar deles, você deve salvá-los antes de fazer isso.

    Essa é a abordagem geral – na maioria dos casos, você não precisará definir o $ IFS para um valor diferente.

    Algumas outras alternativas fornecidas pelo usuário incluem:

     read ABCD < < HERE $(IFS=.; echo $(/usr/local/bin/ipaddr)) HERE 

    e, quando a substituição do processo estiver disponível,

     read ABCD < <(IFS=.; echo $(/usr/local/bin/ipaddr)) 

    Hmmm … Eu quase juraria que isso funcionou para o shell Bourne original, mas não tenho access a uma cópia em execução agora para checar.

    Há, no entanto, uma solução muito trivial para o problema.

    Altere a primeira linha do script de:

     #!/bin/bash 

    para

     #!/bin/ksh 

    E voila! Uma leitura no final de um pipeline funciona muito bem, supondo que você tenha o shell Korn instalado.

    Essa é uma pergunta interessante e aborda um conceito muito básico no Bourne shell e no subshell. Aqui eu forneço uma solução diferente das soluções anteriores, fazendo algum tipo de filtragem. Vou dar um exemplo que pode ser útil na vida real. Este é um fragment para verificar os arquivos baixados em conformidade com as sums de verificação conhecidas. O arquivo de sum de verificação se parece com o seguinte (Mostrando apenas 3 linhas):

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

    O script de 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 

    O pai e o subshell se comunicam através do comando echo. Você pode escolher um texto fácil de analisar para o shell pai. Este método não quebra seu modo normal de pensar, apenas que você precisa fazer algum processamento posterior. Você pode usar grep, sed, awk e muito mais para fazer isso.

    Que tal um método muito simples?

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