El incremento del contador en el bucle Bash no funciona

125

Tengo el siguiente script simple donde estoy ejecutando un bucle y quiero mantener a COUNTER. No puedo entender por qué el contador no se está actualizando. ¿Se debe a la subshell que se está creando? ¿Cómo puedo solucionar esto 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
Sparsh Gupta
fuente
1
Relacionado: stackoverflow.com/questions/13726764/…
Gabriel Devillers
No necesita poner el bucle while en la subshell. Simplemente quite los corchetes mientras el bucle es suficiente. O bien, si debe ponerlo en bucle en la subshell, luego, mientras lo hace, volcar el contador en el archivo temporal una vez y restaurar este archivo fuera de la subshell. Te prepararé el procedimiento final en respuesta.
Znik

Respuestas:

156

Primero, no estás aumentando el contador. Cambiando COUNTER=$((COUNTER))en COUNTER=$((COUNTER + 1))o COUNTER=$[COUNTER + 1]aumentará ella.

En segundo lugar, es más difícil propagar hacia atrás las variables del subshell a la persona que llama a medida que usted supone. Las variables en una subshell no están disponibles fuera de la subshell. Estas son variables locales al proceso hijo.

Una forma de resolverlo es usar un archivo temporal para almacenar el valor 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
bos
fuente
30
$ [...] está en desuso.
chepner
1
@chepner ¿Tiene una referencia que dice que $[...]está en desuso? ¿Hay una solución alternativa?
blong
9
$[...]fue utilizado por bashantes $((...))fue adoptado por el shell POSIX. No estoy seguro de que alguna vez se haya desaprobado formalmente, pero no puedo encontrar ninguna mención al respecto en la bashpágina de manual, y parece que solo es compatible con la compatibilidad con versiones anteriores.
chepner
Además, se prefiere $ (...) sobre...
Lennart Rolland
77
@blong Aquí hay una pregunta SO sobre $ [...] vs $ ((...)) que discute y hace referencia a la desaprobación: stackoverflow.com/questions/2415724/…
Ogre Psalm33
87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

BASH PROBADO: Centos, SuSE, RH

Jay Stan
fuente
1
@kroonwijk debe haber un espacio antes del corchete (para "delimitar las palabras", hablando formalmente). Bash no puede ver el final de la expresión anterior.
EdwardGarson
1
las preguntas fueron sobre un tiempo con una tubería, por lo que cuando se crea una subshell, su respuesta es correcta, pero no usa una tubería, por lo que no responde la pregunta
chrisweb
2
Según el comentario de chepner sobre otra respuesta, la $[ ]sintaxis está en desuso. stackoverflow.com/questions/10515964/…
Mark Haferkamp
esto no resuelve la pregunta principal, el bucle principal se coloca debajo de la subshell
Znik
42
COUNTER=$((COUNTER+1)) 

Es una construcción bastante torpe en la programación moderna.

(( COUNTER++ ))

se ve más "moderno". También puedes usar

let COUNTER++

si crees que eso mejora la legibilidad. A veces, Bash ofrece demasiadas formas de hacer las cosas, supongo que la filosofía de Perl, cuando quizás la Python "solo hay una manera correcta de hacerlo" podría ser más apropiada. ¡Esa es una declaración discutible si alguna vez hubo una! De todos modos, sugeriría que el objetivo (en este caso) no es solo incrementar una variable sino (regla general) también escribir código que alguien más pueda entender y apoyar. La conformidad hace mucho para lograrlo.

HTH

Bill Parker
fuente
Esto no aborda la pregunta original, que es cómo obtener el valor actualizado en el contador DESPUÉS de finalizar el ciclo (subproceso)
Luis Vázquez
16

Tratar de usar

COUNTER=$((COUNTER+1))

en vez de

COUNTER=$((COUNTER))
dbf
fuente
8
o simplementelet "COUNTER++"
nullpotent
2
Lo siento, fue un error tipográfico. Es en realidad ((COUNTER + 1))
Sparsh Gupta
8
@AaronDigulla: (( COUNTER++ ))(sin signo de dólar)
Pausado hasta nuevo aviso.
2
No estoy seguro de por qué, pero veo que un script mío falla repetidamente cuando lo uso, (( COUNTER++ ))pero cuando lo cambié COUNTER=$((COUNTER + 1))funcionó. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Steven Lu
¿Tal vez su línea de hash bang ejecuta bash como / bin / sh en lugar de / bin / bash?
Max
12

Creo que esta única llamada awk es equivalente a tu grep|grep|awk|awk canalización: pruébelo. Su último comando awk parece no cambiar nada en absoluto.

El problema con COUNTER es que el ciclo while se ejecuta en una subshell, por lo que cualquier cambio en la variable desaparecerá cuando la subshell salga. Debe acceder al valor de COUNTER en esa misma subshell. O tome el consejo de @ DennisWilliamson, use una sustitución de proceso y evite la subshell por completo.

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
}
Glenn Jackman
fuente
1
Gracias, el último awk básicamente eliminará todo después de end = 1 y pondrá un nuevo end = 1 al final (para que la próxima vez podamos eliminar todo lo que se agrega después).
Sparsh Gupta
1
@SparshGupta, el awk anterior no imprime nada después de "end = 1".
Glenn Jackman
Esto mejora muy bien el script de preguntas, pero no resuelve el problema de aumentar el contador dentro de la subshell
Znik
12
count=0   
base=1
(( count += base ))
pkm
fuente
11

En lugar de utilizar un archivo temporal, puede evitar crear una subshell alrededor del whilebucle mediante la sustitución del proceso.

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

Por cierto, deberías poder transformar todo eso grep, grep, awk, awk, awken un soloawk .

Comenzando con Bash 4.2, hay una lastpipeopción que

ejecuta el último comando de una tubería en el contexto actual del shell. La opción lastpipe no tiene efecto si el control de trabajo está habilitado.

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
Pausado hasta nuevo aviso.
fuente
la sustitución de procesos es excelente si desea incrementar un contador dentro del bucle y usarlo afuera cuando termine, el problema con las sustituciones de proceso es que no encontré la manera de obtener también el código de estado del comando ejecutado, que es posible cuando se usa una tubería utilizando $ {PIPESTATUS [*]}
chrisweb
@chrisweb: agregué información sobre lastpipe. Por cierto, probablemente deberías usar "${PIPESTATUS[@]}"(en lugar de asterisco).
Pausado hasta nuevo aviso.
errata en bash (no en perl como he escrito anteriormente por error) el código de salida es una tabla, luego puede verificar por separado todos los códigos de salida en la cadena de tuberías. antes de probar primero, su paso debe ser copiar esta tabla; de lo contrario, después del primer comando, perderá todos los valores.
Znik
Esta es la solución que funcionó para mí y sin usar un archivo externo para almacenar el valor de la variable, que en mi opinión es demasiado peatonal.
Luis Vázquez
8

minimalista

counter=0
((counter++))
echo $counter
geekzspot
fuente
Uno simple :-). Gracias @geekzspot
Hussain K
no funciona, por ejemplo, en cuestión, porque hay subshell
Znik
3

Esto es todo lo que necesitas hacer:

$((COUNTER++))

Aquí hay un extracto de Learning the bash Shell , 3rd Edition, pp. 147, 148:

Las expresiones aritméticas bash son equivalentes a sus contrapartes en los lenguajes Java y C. [9] La precedencia y la asociatividad son las mismas que en C. La tabla 6-2 muestra los operadores aritméticos que son compatibles. Aunque algunos de estos son (o contienen) caracteres especiales, no es necesario hacer una barra invertida para escapar de ellos, ya que están dentro de la sintaxis $ ((...)).

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

Los operadores ++ y - son útiles cuando desea aumentar o disminuir un valor en uno. [11] Funcionan igual que en Java y C, por ejemplo, el valor ++ incrementa el valor en 1. Esto se llama post-incremento ; también hay un incremento previo : ++ valor . La diferencia se hace evidente con un ejemplo:

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

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

CE Montijo
fuente
Esta es la versión de esto que necesitaba, porque la estaba usando en la condición de una ifdeclaración: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi correcta o incorrecta, esta es la única versión que funcionó de manera confiable.
LS
Lo importante de este formulario es que puede usar un incremento en un solo paso. i=1; while true; do echo $((i++)); sleep .1; done
Bruno Bronosky
1
@LS: if (( needsComma++ > 0 )); thenoif (( needsComma++ )); then
Pausado hasta nuevo aviso.
Usando "echo $ ((i ++))" en bash siempre obtengo "/opt/xyz/init.sh: línea 29: i: comando no encontrado" ¿Qué estoy haciendo mal?
mmo
Esto no responde a la pregunta sobre cómo obtener el valor del contador fuera del ciclo.
Luis Vázquez
1

Este es un ejemplo simple

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done
zwitterion
fuente
1
ejemplo simple, pero no aplicable a la pregunta.
Znik
0

Parece que no actualizaste counterel script, usacounter++

yjshen
fuente
Disculpas por el error tipográfico, en realidad estoy usando ((contador + 1)) en escritura que no está trabajando
Sparsh Gupta
no importa si se incrementa en valor + 1 o en valor ++. Una vez que finaliza la subshell, el valor del contador se pierde y vuelve al valor 0 inicial establecido al inicio en este script.
Znik
0

Hubo dos condiciones que hicieron que la expresión ((var++))fallara para mí:

  1. Si configuro bash en modo estricto ( set -euo pipefail) y si comienzo mi incremento en cero (0).

  2. Comenzar en uno (1) está bien, pero cero hace que el incremento devuelva "1" al evaluar "++", que es un error de código de retorno distinto de cero en modo estricto.

Puedo usar ((var+=1))o var=$((var+1))escapar de este comportamiento

Augustus Hill
fuente
0

El script de origen tiene algún problema con subshell. Primer ejemplo, probablemente no necesite subshell. Pero no sabemos qué se esconde bajo "Algo más de acción". La respuesta más popular tiene un error oculto, que aumentará la E / S y no funcionará con subshell, porque restaura el interior del bucle.

No agregue el signo '\', informará al intérprete bash sobre la continuación de la línea. Espero que te ayude a ti o a cualquiera. Pero en mi opinión, esta secuencia de comandos debe convertirse completamente a la secuencia de comandos AWK, o reescribirse a Python usando regexp, o perl, pero la popularidad de perl a lo largo de los años se degrada. Mejor hacerlo con python.

Versión corregida sin 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

Versión con subshell si es realmente necesario

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