Incremento contatore nel loop Bash non funzionante


125

Ho il seguente semplice script in cui sto eseguendo un ciclo e voglio mantenere un COUNTER. Non riesco a capire perché il contatore non si aggiorna. È dovuto alla subshell che viene creata? Come posso risolvere questo problema?

#!/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


Non è necessario mettere loop while in subshell. Basta rimuovere le parentesi intorno mentre il ciclo, è sufficiente. Altrimenti, se è necessario inserirlo in subshell, quindi dopo averlo fatto, scaricare il contatore in un file temporaneo una volta e ripristinare questo file al di fuori di subshell. Ti preparerò la procedura finale in risposta.
Znik,

Risposte:


156

Innanzitutto, non stai aumentando il contatore. Cambiando COUNTER=$((COUNTER))in COUNTER=$((COUNTER + 1))o COUNTER=$[COUNTER + 1]lo aumenterà.

In secondo luogo, è più complicato propagare indietro le variabili subshell fino al momento in cui si ipotizza. Le variabili in una subshell non sono disponibili al di fuori della subshell. Queste sono variabili locali per il processo figlio.

Un modo per risolverlo è utilizzare un file temporaneo per memorizzare il valore intermedio:

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
$ [...] è obsoleto.
Chepner,

1
@chepner Hai un riferimento che dice che $[...]è deprecato? C'è una soluzione alternativa?
blong,

9
$[...]è stato utilizzato da bashprima è $((...))stato adottato dalla shell POSIX. Non sono sicuro che sia mai stato formalmente deprecato, ma non riesco a trovarlo nella bashpagina man e sembra essere supportato solo per la compatibilità con le versioni precedenti.
Chepner,

Inoltre, $ (...) è preferito rispetto a...
Lennart Rolland

7
@blong Qui è una domanda SO su $ [...] vs $ ((...)) che discute e riferimenti alla deprecazione: stackoverflow.com/questions/2415724/...
Ogre Psalm33

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

BASH TESTATO: Centos, SuSE, RH


1
@kroonwijk deve esserci uno spazio prima della parentesi quadra (per "delimitare le parole", parlando formalmente). Bash non può altrimenti vedere la fine dell'espressione precedente.
EdwardGarson,

1
le domande riguardavano un po 'di tempo con una pipe, quindi dove viene creata una subshell, la tua risposta è giusta ma non usi una pipe quindi non risponde alla domanda
chrisweb

2
Per il commento di Chepner su un'altra risposta, la $[ ]sintassi è deprecata. stackoverflow.com/questions/10515964/…
Mark Haferkamp

questo non risolve la domanda principale, il ciclo principale viene inserito in subshell
Znik

42
COUNTER=$((COUNTER+1)) 

è un costrutto piuttosto goffo nella programmazione moderna.

(( COUNTER++ ))

sembra più "moderno". Puoi anche usare

let COUNTER++

se pensi che migliora la leggibilità. A volte, Bash offre troppi modi di fare le cose - immagino la filosofia Perl - quando forse il Python "esiste un solo modo giusto per farlo" potrebbe essere più appropriato. È un'affermazione discutibile se mai ce n'è stata una! Ad ogni modo, suggerirei che l'obiettivo (in questo caso) non è solo quello di incrementare una variabile ma (regola generale) anche di scrivere codice che qualcun altro possa comprendere e supportare. La conformità fa molto per raggiungerlo.

HTH


Questo non affronta la domanda originale, ovvero come ottenere il valore aggiornato nel contatore DOPO la fine del ciclo (sottoprocesso)
Luis Vazquez,

16

Prova ad usare

COUNTER=$((COUNTER+1))

invece di

COUNTER=$((COUNTER))

8
o sololet "COUNTER++"
nullpotent

2
Scusa, era un errore di battitura. È effettivamente ((COUNTER + 1))
Sparsh Gupta

8
@AaronDigulla: (( COUNTER++ ))(nessun segno di dollaro)
In pausa fino a nuovo avviso.

2
Non sono sicuro del perché, ma vedo una mia sceneggiatura fallire ripetutamente durante l'utilizzo (( COUNTER++ ))ma quando sono passato a COUNTER=$((COUNTER + 1))esso ha funzionato. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu,

Forse la tua linea di hash bang funziona bash come / bin / sh anziché / bin / bash?
Max

12

Penso che questa singola chiamata awk sia equivalente alla tua grep|grep|awk|awkpipeline: per favore testala. Il tuo ultimo comando awk sembra non cambiare nulla.

Il problema con COUNTER è che il ciclo while è in esecuzione in una subshell, quindi qualsiasi modifica alla variabile scompare quando la subshell esce. È necessario accedere al valore di COUNTER nella stessa sottostruttura. Oppure segui il consiglio di @ DennisWilliamson, usa una sostituzione di processo ed evita del tutto la sottotitoli.

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
Grazie, l'ultimo awk rimuoverà praticamente tutto dopo end = 1 e inserirà un nuovo end = 1 alla fine (in modo che la prossima volta possiamo rimuovere tutto ciò che viene aggiunto dopo di esso).
Sparsh Gupta,

1
@SparshGupta, il awk precedente non stampa nulla dopo "end = 1".
Glenn Jackman,

Questo migliora molto per lo script di domande, ma non risolve il problema con l'aumento del contatore all'interno della subshell
Znik

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

11

Invece di utilizzare un file temporaneo, è possibile evitare di creare una subshell attorno al whileciclo utilizzando la sostituzione del processo.

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

A proposito, dovresti essere in grado di trasformare tutto ciò grep, grep, awk, awk, awkin un unicoawk .

A partire da Bash 4.2, c'è lastpipeun'opzione che

esegue l'ultimo comando di una pipeline nel contesto della shell corrente. L'opzione lastpipe non ha alcun effetto se il controllo lavoro è abilitato.

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

la sostituzione di processo è ottima se si desidera incrementare un contatore all'interno del ciclo e usarlo all'esterno quando fatto, il problema con le sostituzioni di processo è che non ho trovato alcun modo per ottenere anche il codice di stato del comando eseguito, che è possibile quando si utilizza una pipe usando $ {PIPESTATUS [*]}
chrisweb l'

@chrisweb: ho aggiunto informazioni su lastpipe. A proposito, probabilmente dovresti usare "${PIPESTATUS[@]}"(al posto dell'asterisco).
In pausa fino a nuovo avviso.

errata. in bash (non in perl come ho scritto in precedenza per errore) il codice di uscita è una tabella, quindi è possibile controllare separatamente tutti i codici di uscita nella catena di tubi. prima di provare prima il tuo passo deve essere copiare questa tabella, altrimenti dopo il primo comando perderai tutti i valori.
Znik,

Questa è la soluzione che ha funzionato per me e senza usare un file esterno per archiviare il valore della variabile che è troppo pedonale secondo me.
Luis Vazquez,

8

minimalista

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

Semplice :-). Grazie @geekzspot
Hussain K

non funziona per esempio in questione, perché c'è subshell
Znik

3

Questo è tutto ciò che devi fare:

$((COUNTER++))

Ecco un estratto da Learning the bash Shell , 3rd Edition, pp. 147, 148:

le espressioni aritmetiche bash sono equivalenti alle loro controparti nei linguaggi Java e C. [9] Precedenza e associatività sono le stesse di C. La tabella 6-2 mostra gli operatori aritmetici supportati. Sebbene alcuni di questi siano (o contengano) caratteri speciali, non è necessario eseguirne il backslash, poiché rientrano nella sintassi $ ((...)).

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

Gli operatori ++ e - sono utili quando si desidera incrementare o decrementare un valore di uno. [11] Funzionano come in Java e C, ad es. Valore ++ incrementi valore di 1. Questo si chiama post-incremento ; c'è anche un pre-incremento : ++ valore . La differenza diventa evidente con un esempio:

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

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


Questa è la versione di cui avevo bisogno, perché la stavo usando nella condizione di una ifdichiarazione: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi giusta o sbagliata, questa è l'unica versione che ha funzionato in modo affidabile.
LS

La cosa importante di questo modulo è che puoi usare un incremento in un solo passaggio. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky,

1
@LS: if (( needsComma++ > 0 )); thenoppureif (( needsComma++ )); then
In pausa fino a ulteriore avviso.

Usando "echo $ ((i ++))" in bash ottengo sempre "/opt/xyz/init.sh: linea 29: i: comando non trovato" Cosa sto facendo di sbagliato?
mmo

Questo non affronta la domanda su come ottenere il valore del contatore al di fuori del ciclo.
Luis Vazquez,

1

Questo è un semplice esempio

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

1
semplice esempio, ma non applicabile alla domanda.
Znik,

0

Sembra che non sia stato aggiornato counterlo script, usarecounter++


Mi scuso per l'errore di battitura, in realtà sto usando ((COUNTER + 1)) nella sceneggiatura che non funziona
Sparsh Gupta

non importa se è incremettato dal valore + 1 o dal valore ++. Al termine della subshell, il valore del contatore viene perso e ripristina il valore iniziale 0 impostato all'inizio su questo script.
Znik,

0

Ci sono state due condizioni che hanno causato il ((var++))fallimento dell'espressione per me:

  1. Se imposto bash su modalità rigorosa ( set -euo pipefail) e se inizio il mio incremento a zero (0).

  2. Iniziare da uno (1) va bene ma zero fa sì che l'incremento restituisca "1" quando si valuta "++" che è un errore di codice di ritorno diverso da zero in modalità rigorosa.

Posso usare ((var+=1))o var=$((var+1))per sfuggire a questo comportamento


0

Lo script di origine ha qualche problema con subshell. Primo esempio, probabilmente non hai bisogno di subshell. Ma non sappiamo cosa sia nascosto sotto "Qualche altra azione". La risposta più popolare ha un bug nascosto, che aumenterà l'I / O e non funzionerà con la subshell, perché ripristina il ciclo interno.

Non aggiungere alcun segno "\", informerà l'interprete bash sulla continuazione della linea. Spero che possa aiutare te o qualcuno. Ma secondo me questo script dovrebbe essere completamente convertito in script AWK, oppure riscritto in python usando regexp o perl, ma la popolarità del perl negli anni è degradata. Meglio farlo con Python.

Versione corretta senza subshell:

#!/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

Versione con subshell se è veramente necessaria

#!/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.