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


187

No programa a seguir, se eu definir a variável $foocomo o valor 1 na primeira ifinstrução, ela funcionará no sentido de que seu valor será lembrado após a instrução if. No entanto, quando defino a mesma variável com o valor 2 dentro de um ifque está dentro de uma whileinstrução, ela é esquecida após o whileloop. Está se comportando como se eu estivesse usando algum tipo de cópia da variável $foodentro do whileloop e estou modificando apenas essa cópia específica. 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)


O utilitário shellcheck captura isso (consulte github.com/koalaman/shellcheck/wiki/SC2030 ); Recortar e colar o código acima no shellcheck.net emite esse feedback para a Linha 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Respostas:


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

O whileloop é executado em um subshell. Portanto, quaisquer alterações feitas na variável não estarão disponíveis quando o subshell terminar.

Em vez disso, você pode usar uma string here para reescrever o loop while para estar no processo principal do shell; somente echo -e $linesserá executado 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")"

Você pode se livrar do feio echona string here acima, expandindo as seqüências de barra invertida imediatamente ao atribuir lines. A $'...'forma de citação pode ser usada lá:

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

20
melhor mudar <<< "$(echo -e "$lines")"para simples<<< "$lines"
Beliy

e se a fonte fosse em tail -fvez de texto fixo?
mt eee

2
@mteee Você pode usar while read -r line; do echo "LINE: $line"; done < <(tail -f file)(obviamente o loop não será encerrado, pois continua aguardando a entrada de tail).
PP

Estou limitado a / bin / sh - existe uma maneira alternativa que funcione no shell antigo?
user9645

1
@AvinashYadav O problema não está realmente relacionado ao whileloop ou forloop; em vez disso, o uso do subshell, ou seja, em cmd1 | cmd2, cmd2está em um subshell. Portanto, se um forloop for executado em uma subshell, o comportamento inesperado / problemático será exibido.
PP

48

ATUALIZADO # 2

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

Soluções alternativas:

Eliminar echo

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

Adicione o eco dentro do documento aqui é

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

Executar echoem segundo plano:

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

Redirecione para um identificador de arquivo explicitamente (lembre-se do espaço < <!):

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

Ou apenas redirecione para stdin:

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

E um para chepner(eliminando echo):

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

A variável $linespode ser convertida em uma matriz sem iniciar um novo sub-shell. Os caracteres \e ntem de ser convertido para algum personagem (por exemplo, uma verdadeira caractere de nova linha) e usar a variável IFS (Campo Interno 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

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

+1 para o documento aqui, pois o linesúnico objetivo da variável parece alimentar o whileloop.
Chepner # 31/13

@chepner: Thx! Adicionei outro, dedicado a você!
TrueY

Existe ainda outra solução fornecida aqui :for line in $(echo -e $lines); do ... done
dma_k 19/01/16

Obrigado por seu comentário! Esta solução resultaria em 6 linhas contendo uma única palavra. O pedido do OP era diferente ...
TrueY

votado. correndo eco em um dentro subshell aqui está, foi uma das poucas soluções que trabalhou em cinzas
Hamy

9

Você é o 742342º usuário a fazer perguntas frequentes sobre o bash. A resposta também descreve o caso geral de variáveis ​​definidas em subshells criados por pipes:

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

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

Cada elemento de um pipeline, mesmo uma função interna ou shell, é executado em um processo separado, um filho do shell executando o pipeline. Um subprocesso não pode afetar o ambiente de seus pais. Quando o readcomando define a variável como entrada, essa variável é configurada apenas no subshell, não no shell pai. Quando o subshell sai, o valor da variável é perdido.

Muitos pipelines que terminam com read variablepodem ser convertidos em substituições de comando, que capturam a saída de um comando especificado. A saída pode 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 variáveis, como a leitura faz quando recebem vários argumentos de variável. Se você precisar fazer isso, poderá 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 de padrão de 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 A B C D

para dividir 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, salve-os antes de fazer isso.

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

Algumas outras alternativas fornecidas pelo usuário incluem:

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

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

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

7
Você esqueceu o problema do Family Feud . Às vezes, é muito difícil encontrar a mesma combinação de palavras que quem escreveu a resposta, para que você não seja inundado por resultados errados, nem filtre a resposta.
Evi1M4chine 29/01

3

Hmmm ... eu quase juro que isso funcionou para o shell Bourne original, mas não tenho acesso a uma cópia em execução agora para verificar.

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

Altere a primeira linha do script de:

#!/bin/bash

para

#!/bin/ksh

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


1

Eu uso o stderr para armazenar dentro de um loop e ler dele fora. Aqui var i é inicialmente definido e lido dentro do loop como 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

Ou use o método heredoc para reduzir a quantidade de código em um subshell. Observe que o valor iterativo i pode ser lido fora do loop 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

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.

6
Talvez haja uma resposta decente sob a superfície aqui, mas você massacrou a formatação a tal ponto que é desagradável tentar ler.
Mark Amery

Você quer dizer que o código original é agradável de ler? (Eu apenas segui: p)
Marcin

0

Essa é uma pergunta interessante e aborda um conceito muito básico no shell e subshell Bourne. Aqui, 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 fragmento para verificar se os arquivos baixados estão em conformidade com uma soma de verificação conhecida. O arquivo de soma de verificação é semelhante ao 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. Esse método não quebra sua maneira de pensar normal, apenas que você precisa fazer algum pós-processamento. Você pode usar grep, sed, awk e muito mais para fazer isso.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.