bash: variável perde valor no final do loop while read

36

Eu tenho um problema em um dos meus scripts de shell. Perguntei a alguns colegas, mas todos eles apenas balançaram a cabeça (depois de alguns arranhões), então eu vim aqui para uma resposta.

De acordo com o meu entendimento, o seguinte script shell deve imprimir "Count is 5" como a última linha. Exceto que não. Imprime "Contagem é 0". Se o "while read" for substituído por qualquer outro tipo de loop, ele funcionará perfeitamente. Aqui está o script:

eco "1"> input.data
eco "2" >> input.data
eco "3" >> input.data
eco "4" >> input.data
eco "5" >> input.data

CNT = 0 

cat input.data | enquanto lê;
Faz
  deixe CNT ++;
  eco "Contando até $ CNT"
feito 
eco "Contagem é $ CNT"

Por que isso acontece e como posso evitá-lo? Eu tentei isso no Debian Lenny e Squeeze, o mesmo resultado (por exemplo, bash 3.2.39 e bash 4.1.5. Admito totalmente que não sou um assistente de script de shell, para que qualquer ponteiro seja apreciado.

wolfgangsz
fonte

Respostas:

30

Veja o argumento @ Bash FAQ entry # 24: "Defino variáveis ​​em um loop. Por que desaparecem repentinamente após o encerramento do loop? Ou por que não posso canalizar os dados para ler?" (arquivado aqui mais recentemente ).

Resumo: Isso é suportado apenas a partir do bash 4.2 ou superior. Você precisa usar maneiras diferentes, como substituições de comandos, em vez de um pipe, se estiver usando o bash.

Ignacio Vazquez-Abrams
fonte
Você recebe o bônus, já que sua resposta me forneceu a mais ampla gama de opções.
wolfgangsz
5
O link está morto. É por isso que as respostas somente para links são ruins. Pelo menos, resuma a resposta aqui.
Rudolfbyker 29/04
Deus, mais uma vez em que o ksh é simplesmente muito melhor ... por que, exatamente por que todos se reuniram em torno do bash.
Florian Heigl
@FlorianHeigl: Você está reivindicando que o ksh é o One True Shell?
Ignacio Vazquez-Abrams
@ IgnacioVazquez-Abrams não, mas eu afirmo que o processamento de loop while no bash é uma PITA horrivelmente. O manuseio do loop foi o longo prazo, impedindo-o de capturar a funcionalidade de 1993. As outras coisas são manipulação de getopt onde o manipulador embutido (também em 1993) era simples e capaz, algo que você ainda não pode obter a menos que esteja usando o docopt. Estou afirmando que o bash se coloca atrás da curva há mais de 20 anos, insistentemente, e a quantidade de tempo gasto com ISSO AQUI ou milhões de usos ruins de getopts está além da medida - aceita apenas porque a maioria das pessoas nunca saberá.
Florian Heigl
30

Este é um tipo de erro "comum". Os pipes criam SubShells, portanto, eles while readsão executados em um shell diferente do seu script, que faz com que sua CNTvariável nunca seja alterada (apenas aquela dentro do subshell de pipe).

Agrupe o último echocom o subshell whilepara consertá-lo (há muitas outras maneiras de consertá-lo, esse é um. As respostas de Iain e Ignacio têm outras.)

CNT=0

 cat input.data | ( while read 
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT" )

Explicação longa:

  1. Você declara CNTem seu script o valor 0;
  2. Um SubShell é iniciado no |para while read;
  3. Sua $CNTvariável é exportada para o SubShell com o valor 0;
  4. O SubShell conta e aumenta o CNTvalor para 5;
  5. O SubShell termina, as variáveis ​​e os valores são destruídos (eles não retornam ao processo / script de chamada).
  6. Você é o echoseu CNTvalor original de 0.
coredump
fonte
2
O primeiro script de shell que eu escrevi me deu os mesmos problemas, bateu minha cabeça contra a parede por um tempo antes de descobrir que esses canos geram conchas adicionais. Qualquer variável com a qual você mexa em um pipe ficará fora do escopo assim que o pipe terminar - o que significa que, se você realmente quiser fazer algo com uma variável fora do pipe em que foi usado, precisará mantenha o estado através de algo descolado, como um arquivo temporário.
Photoionized
Excelente resposta, infelizmente, só posso dar um bônus de aceitação. Desculpe.
11Preço
10

Isso funciona

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 suporta GoFundMonica
fonte
Gosto disso, da maneira inteligente, porque você sabe onde estão os dados necessários e só precisa recuperá-los. Se você não conhece soluções de alta habilidade, sempre pode "ler um arquivo" hahahha. +1 para você.
M3nda
1
Qualquer pessoa que esteja lendo isso, esteja ciente de que a solução fornecida por Iain só funciona quando você tem seu script invocando explicitamente o bash, com a primeira linha: #! / Bin / bash e que: #! / Bin / sh não funcionará.
Roadowl
1
Interessante, primeiro exemplo que eu já vi onde o Uso Inútil do Gato realmente impedia o funcionamento do código . A propósito, @Roadowl, o único basismo aqui é a linha let CNT++que deveria ser CNT="$((CNT+1))"usada para expansão aritmética compatível com POSIX . O resto já é portátil.
Curinga
6

Tente passar os dados em um sub-shell, como se fosse um arquivo antes do loop while. Isso é semelhante à solução de lain, mas assume que você não deseja um arquivo intermitente:

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"
Steve
fonte