bash: la variable pierde valor al final del ciclo de lectura

36

Tengo un problema en uno de mis scripts de shell. Pregunté a algunos colegas, pero todos menearon la cabeza (después de rascarse), así que he venido aquí por una respuesta.

Según tengo entendido, el siguiente script de shell debería imprimir "Count is 5" como la última línea. Excepto que no. Imprime "El recuento es 0". Si el "while read" se reemplaza con cualquier otro tipo de bucle, funciona bien. Aquí está el guión:

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

CNT = 0 

cat input.data | mientras lee;
hacer
  dejar CNT ++;
  echo "Contando a $ CNT"
hecho 
echo "El recuento es $ CNT"

¿Por qué sucede esto y cómo puedo evitarlo? He intentado esto en Debian Lenny y Squeeze, el mismo resultado (es decir, bash 3.2.39 y bash 4.1.5. Admito plenamente que no soy un asistente de script de shell, por lo que cualquier puntero sería apreciado.

wolfgangsz
fuente

Respuestas:

30

Vea el argumento @ Bash FAQ entrada # 24: "Establezco variables en un ciclo. ¿Por qué desaparecen repentinamente después de que el ciclo termina? O, ¿por qué no puedo canalizar datos para leer?" (archivado más recientemente aquí ).

Resumen: esto solo es compatible desde bash 4.2 y versiones posteriores. Debe usar diferentes formas, como sustituciones de comandos en lugar de una tubería, si está utilizando bash.

Ignacio Vazquez-Abrams
fuente
Obtiene la bonificación, ya que su respuesta me proporcionó la más amplia gama de opciones.
wolfgangsz
55
El enlace está muerto. Es por eso que las respuestas de solo enlace son malas. Al menos resuma la respuesta aquí.
rudolfbyker
Dios, otra vez donde ksh es simplemente mucho mejor ... por qué, solo por qué todos acudieron en masa a bash.
Florian Heigl
@FlorianHeigl: ¿Estás afirmando que ksh es el One True Shell?
Ignacio Vazquez-Abrams
@ IgnacioVazquez-Abrams no, pero afirmo que el manejo del bucle while en bash es terriblemente PITA. El manejo del bucle fue a largo plazo evitando que se pusiera al día con la funcionalidad de 1993. Las otras cosas son el manejo de getopt donde el (también 1993) controlador incorporado era simple y capaz, algo que aún no puede obtener a menos que use, es decir, docopt. Estoy afirmando que bash se ha puesto detrás de la curva durante más de 20 años, insistentemente y la cantidad de tiempo dedicado a ESTA COSA AQUÍ o millones de malos usos de getopts está fuera de medida, solo aceptado porque la mayoría de la gente nunca lo sabrá.
Florian Heigl
30

Esto es una especie de error 'común'. Las tuberías crean SubShells, por lo que while readse ejecuta en un shell diferente al de su script, lo que hace que su CNTvariable nunca cambie (solo la que está dentro del subshell de la tubería).

Agrupe el último echocon el subshell whilepara arreglarlo (hay muchas otras formas de arreglarlo, esta es una. Las respuestas de Iain e Ignacio tienen otras).

CNT=0

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

Larga explicación:

  1. Usted declara CNTen su script como valor 0;
  2. Un subnivel se inicia en el |a while read;
  3. Su $CNTvariable se exporta a SubShell con el valor 0;
  4. SubShell cuenta y aumenta el CNTvalor a 5;
  5. SubShell finaliza, las variables y los valores se destruyen (no vuelven al proceso / script de llamada).
  6. Usted echosu CNTvalor original de 0.
volcado de memoria
fuente
2
El primer guión de shell que escribí me dio los mismos problemas, me golpeé la cabeza contra la pared por un tiempo antes de descubrir que esas tuberías generan conchas adicionales. Cualquier variable con la que te metas en una tubería quedará fuera de alcance tan pronto como termine la tubería, lo que significa que si realmente quieres hacer algo con una variable fuera de la tubería en la que se usó, tendrás que mantenga el estado a través de algo funky como un archivo temporal.
fotoionizada el
Excelente respuesta, desafortunadamente solo puedo dar un bono de aceptación. Lo siento.
wolfgangsz
10

Esto funciona

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517 es compatible con GoFundMonica
fuente
Me gusta, la forma inteligente porque sabes dónde están los datos necesarios y solo necesitas recuperarlos. Si no conoce soluciones de alta habilidad, siempre puede "leer un archivo" jajajaja. +1 para ti
m3nda
1
Cualquiera que lea esto, tenga en cuenta que la solución proporcionada por Iain solo funciona cuando su script invoca explícitamente bash, al tener la primera línea: #! / Bin / bash y que: #! / Bin / sh no funcionará.
Roadowl
1
Interesante, primer ejemplo que vi cuando un uso inútil de Cat realmente impidió que el código funcionara . Por cierto @Roadowl, el único bashism aquí es la línea let CNT++que debería ser CNT="$((CNT+1))"para usar la expansión aritmética compatible con POSIX . El resto ya es portátil.
Comodín el
6

Intente pasar los datos en un subconjunto, como si fuera un archivo antes del ciclo while. Esto es similar a la solución de lain, pero supone que no desea algún archivo 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
fuente