O incremento do contador no loop Bash não está funcionando


125

Eu tenho o seguinte script simples em que estou executando um loop e quero manter um COUNTER. Não consigo descobrir por que o contador não está atualizando. É devido ao subshell que está sendo criado? Como posso corrigir isso potencialmente?

#!/bin/bash

WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' | awk -F ', ' '{print $2,$4,$0}' | awk '{print "http://domain.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' | awk -F '&end=1' '{print $1"&end=1"}' |
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
)

echo $COUNTER # output = 0


Você não precisa colocar o loop while no subshell. Basta remover os suportes ao redor do loop while, é o suficiente. Ou então, se você precisar colocá-lo em loop no subshell, depois de terminar, despeje o contador no arquivo temporário uma vez e restaure esse arquivo fora do subshell. Vou preparar o procedimento final para você em resposta.
Znik 22/01

Respostas:


156

Primeiro, você não está aumentando o contador. Mudar COUNTER=$((COUNTER))para COUNTER=$((COUNTER + 1))ou COUNTER=$[COUNTER + 1]aumentará.

Segundo, é mais difícil propagar novamente as variáveis ​​do subshell para o receptor conforme você supõe. Variáveis ​​em um subshell não estão disponíveis fora do subshell. Essas são variáveis ​​locais para o processo filho.

Uma maneira de resolvê-lo é usar um arquivo temporário para armazenar o valor intermediário:

TEMPFILE=/tmp/$$.tmp
echo 0 > $TEMPFILE

# Loop goes here
  # Fetch the value and increase it
  COUNTER=$[$(cat $TEMPFILE) + 1]

  # Store the new value
  echo $COUNTER > $TEMPFILE

# Loop done, script done, delete the file
unlink $TEMPFILE

30
$ [...] está obsoleto.
Chepner

1
@chepner Você tem uma referência que diz $[...]estar obsoleta? Existe uma solução alternativa?
blong 27/05

9
$[...]foi usado por bashantes $((...))foi adotado pelo shell POSIX. Não tenho certeza de que alguma vez foi formalmente reprovado, mas não encontro menção a isso na bashpágina de manual e parece ser suportado apenas para compatibilidade com versões anteriores.
Chepner 27/05

Além disso, $ (...) é preferido sobre...
Lennart Rolland

7
@blong Aqui está uma pergunta SO sobre $ [...] vs $ ((...)) que discute e faz referência à depreciação: stackoverflow.com/questions/2415724/…
Ogre Psalm33 /

87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

FESTA TESTADA: Centos, SuSE, RH


1
@kroonwijk precisa haver um espaço antes do colchete (para 'delimitar as palavras', formalmente falando). Caso contrário, o Bash não poderá ver o final da expressão anterior.
EdwardGarson

1
as perguntas era sobre um tempo com um tubo, por isso, onde um subshell é criado, a sua resposta é certo, mas você não usar um tubo para que ele não está respondendo à pergunta
chrisweb

2
Por comentário do chepner em outra resposta, a $[ ]sintaxe foi preterida. stackoverflow.com/questions/10515964/…
Mark Haferkamp 08/08/19

isso não resolve a questão principal, o loop principal é colocado no subshell
Znik

42
COUNTER=$((COUNTER+1)) 

é uma construção bastante desajeitada na programação moderna.

(( COUNTER++ ))

parece mais "moderno". Você também pode usar

let COUNTER++

se você acha que melhora a legibilidade. Às vezes, Bash oferece muitas maneiras de fazer as coisas - filosofia Perl, suponho - quando talvez o Python "só existe uma maneira correta de fazer isso" possa ser mais apropriado. Essa é uma afirmação discutível, se é que houve alguma! De qualquer forma, eu sugeriria que o objetivo (neste caso) não é apenas incrementar uma variável, mas (regra geral) também escrever código que outra pessoa possa entender e apoiar. A conformidade contribui muito para conseguir isso.

HTH


Isso não aborda a questão original, que é como obter o valor atualizado no contador APÓS o término do loop (subprocesso)
Luis Vazquez

16

Tente usar

COUNTER=$((COUNTER+1))

ao invés de

COUNTER=$((COUNTER))

8
ou apenaslet "COUNTER++"
nullpotent

2
Desculpe, foi um erro de digitação. É realmente ((CONTADOR + 1))
Sparsh Gupta

8
@AaronDigulla: (( COUNTER++ ))(sem cifrão)
pausado até novo aviso.

2
Não sei por que, mas estou vendo um script meu repetidamente falhar ao usar, (( COUNTER++ ))mas quando mudei para COUNTER=$((COUNTER + 1))ele funcionou. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu

Talvez sua linha de hash bang execute bash como / bin / sh em vez de / bin / bash?
Max

12

Acho que essa chamada única do awk é equivalente ao seu grep|grep|awk|awkpipeline: teste-a. Seu último comando awk parece não mudar nada.

O problema com o COUNTER é que o loop while está sendo executado em um subshell, portanto, quaisquer alterações na variável desaparecem quando o subshell sai. Você precisa acessar o valor de COUNTER no mesmo subshell. Ou siga o conselho de @ DennisWilliamson, use uma substituição de processo e evite completamente o subshell.

awk '
  /GET \/log_/ && /upstream timed out/ {
    split($0, a, ", ")
    split(a[2] FS a[4] FS $0, b)
    print "http://example.com" b[5] "&ip=" b[2] "&date=" b[7] "&time=" b[8] "&end=1"
  }
' | {
    while read WFY_URL
    do
        echo $WFY_URL #Some more action
        (( COUNTER++ ))
    done
    echo $COUNTER
}

1
Obrigado, o último awk basicamente removerá tudo após o final = 1 e colocará um novo final = 1 no final (para que da próxima vez possamos remover tudo o que for anexado após ele).
Sparsh Gupta

1
@SparshGupta, o awk anterior não imprime nada após "end = 1".
Glenn Jackman

Isso melhora muito o script de pergunta, mas não resolve o problema com o aumento do contador dentro do subshell
Znik

12
count=0   
base=1
(( count += base ))

11

Em vez de usar um arquivo temporário, você pode evitar a criação de um subshell ao redor do whileloop usando a substituição do processo.

while ...
do
   ...
done < <(grep ...)

A propósito, você deve conseguir transformar tudo isso grep, grep, awk, awk, awkem um único awk.

Começando com o Bash 4.2, existe uma lastpipeopção que

executa o último comando de um pipeline no contexto atual do shell. A opção lastpipe não terá efeito se o controle do trabalho estiver ativado.

bash -c 'echo foo | while read -r s; do c=3; done; echo "$c"'

bash -c 'shopt -s lastpipe; echo foo | while read -r s; do c=3; done; echo "$c"'
3

A substituição do processo é excelente se você deseja incrementar um contador dentro do loop e usá-lo fora quando concluído, o problema com as substituições do processo é que não encontrei nenhuma maneira de obter também o código de status do comando executado, o que é possível ao usar um pipe usando $ {PIPESTATUS [*]}
chrisweb 11/11

@ chrisweb: eu adicionei informações sobre lastpipe. A propósito, você provavelmente deve usar "${PIPESTATUS[@]}"(em vez de asterisco).
Pausado até novo aviso.

errata. no bash (não no perl, como escrevi anteriormente por engano), o código de saída é uma tabela; então, você pode verificar separadamente todos os códigos de saída na cadeia de tubulação. antes de testar primeiro, sua etapa deve ser copiada nesta tabela; caso contrário, após o primeiro comando, você perderá todos os valores.
Znik 03/02

Esta é a solução que funcionou para mim e sem o uso de um arquivo externo para armazenar o valor da variável, que é muito pedestre na minha opinião.
Luis Vazquez

8

minimalista

counter=0
((counter++))
echo $counter

Simples :-). Obrigado @geekzspot
Hussain K

não funciona, por exemplo, em questão, porque existe
sub

3

É tudo o que você precisa fazer:

$((COUNTER++))

Aqui está um trecho de Learning the bash Shell , 3a edição, pp. 147, 148:

expressões aritméticas bash são equivalentes às suas contrapartes nas linguagens Java e C. [9] Precedência e associatividade são as mesmas que em C. A Tabela 6-2 mostra os operadores aritméticos suportados. Embora alguns desses sejam (ou contenham) caracteres especiais, não há necessidade de escapar com uma barra invertida, porque eles estão dentro da sintaxe $ ((...)).

..........................

Os operadores ++ e - são úteis quando você deseja aumentar ou diminuir um valor em um. [11] Eles funcionam da mesma forma que em Java e C, por exemplo, o valor ++ incrementa o valor em 1. Isso é chamado pós-incremento ; há também um pré-incremento : ++ valor . A diferença se torna evidente com um exemplo:

$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

Consulte http://www.safaribooksonline.com/a/learning-the-bash/7572399/


Esta é a versão que eu precisava, porque a estava usando na condição de uma ifdeclaração: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi Certo ou errado, esta é a única versão que funcionou de maneira confiável.
LS

O importante deste formulário é que você pode usar um incremento em uma única etapa. i=1; while true; do echo $((i++)); sleep .1; done
de Bruno Bronosky

1
@LS: if (( needsComma++ > 0 )); thenouif (( needsComma++ )); then
Pausado até novo aviso.

Usando "echo $ ((i ++))" no bash, eu sempre recebo "/opt/xyz/init.sh: linha 29: i: comando não encontrado" O que estou fazendo de errado?
Mmo

Isso não aborda a questão de obter o valor do contador fora do loop.
Luis Vazquez

1

Este é um exemplo simples

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done

1
exemplo simples, mas não aplicável à pergunta.
Znik 22/01

0

Parece que você não atualizou countero script, usecounter++


Desculpas para o erro de digitação, eu estou realmente usando ((CONTADOR + 1)) em script que não está funcionando
Sparsh Gupta

não importa se é incrementado pelo valor + 1 ou pelo valor ++. Depois que o subshell termina, o valor do contador é perdido e reverte para o valor 0 inicial definido no início deste script.
Znik 22/01

0

Houve duas condições que causaram uma ((var++))falha na expressão :

  1. Se eu definir o bash no modo estrito ( set -euo pipefail) e se eu iniciar o incremento em zero (0).

  2. Iniciar um (1) é bom, mas zero faz com que o incremento retorne "1" ao avaliar "++", que é uma falha de código de retorno diferente de zero no modo estrito.

Eu posso usar ((var+=1))ou var=$((var+1))escapar desse comportamento


0

O script de origem tem algum problema com o subshell. Primeiro exemplo, você provavelmente não precisa do subshell. Mas não sabemos o que está oculto em "Um pouco mais de ação". A resposta mais popular tem bug oculto, que aumentará a E / S e não funcionará com subshell, porque restaura o loop interno do couter.

Não adicione o sinal '\', ele informará o intérprete do bash sobre a continuação da linha. Espero que ajude você ou qualquer um. Mas, na minha opinião, esse script deve ser totalmente convertido em script AWK, ou reescrito em python usando regexp ou perl, mas a popularidade do perl ao longo dos anos é degradada. Melhor fazê-lo com python.

Versão corrigida sem subcamada:

#!/bin/bash
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
#(  #unneeded bracket
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
# ) unneeded bracket

echo $COUNTER # output = 0

Versão com subshell se for realmente necessário

#!/bin/bash

TEMPFILE=/tmp/$$.tmp  #I've got it from the most popular answer
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
echo $COUNTER > $TEMPFILE  #store counter only once, do it after loop, you will save I/O
)

COUNTER=$(cat $TEMPFILE)  #restore counter
unlink $TEMPFILE
echo $COUNTER # output = 0
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.