¿Por qué mi variable es local en un bucle 'mientras se lee', pero no en otro bucle aparentemente similar?

25

¿Por qué obtengo valores diferentes para $xlos fragmentos a continuación?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?
Peter.O
fuente
Publicación similar en Desbordamiento de pila: no se recuerda una variable modificada dentro de un ciclo while .
codeforester

Respuestas:

26

La explicación correcta ya ha sido dada por jsbillings y geekosaur , pero permítanme ampliarlo un poco.

En la mayoría de los shells, incluido bash, cada lado de una tubería se ejecuta en un subshell, por lo que cualquier cambio en el estado interno del shell (como el establecimiento de variables) permanece limitado a ese segmento de una tubería. La única información que puede obtener de una subshell es lo que genera (a la salida estándar y otros descriptores de archivo) y su código de salida (que es un número entre 0 y 255). Por ejemplo, el siguiente fragmento imprime 0:

a=0; a=1 | a=2; echo $a

En ksh (las variantes derivadas del código AT&T, no las variantes pdksh / mksh) y zsh, el último elemento de una canalización se ejecuta en el shell principal. (POSIX permite ambos comportamientos). Por lo tanto, el fragmento anterior imprime 2.

Una expresión útil es incluir la continuación del ciclo while (o lo que sea que tenga en el lado derecho de la tubería, pero un ciclo while es realmente común aquí) en la tubería:

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}
Gilles 'SO- deja de ser malvado'
fuente
1
Gracias Gilles .. Que a = 0; a = 1 | a = 2 da una imagen muy clara ... y no solo de la localización del estado interno, sino también de que una tubería no necesita enviar nada a través de la tubería (que no sea el código de salida (?) ... En sí mismo eso es una idea interesante de una tubería ... < <(locate -ber ^\.tag$)Logré ejecutar mi script con , gracias a la respuesta original poco clara y los comentarios de geekosaur y glenn jackman ... Inicialmente tuve un dilema sobre aceptar la respuesta, pero el resultado neto fue bastante claro, especialmente con el comentario de seguimiento de jsbillings :)
Peter.O
parece que me conecté a una función, así que moví algunas variables y pruebas a su interior y funcionó muy bien, ¡gracias!
Acuario Power
8

Se encuentra con un problema de alcance variable. Las variables definidas en el ciclo while que está en el lado derecho de la tubería tienen su propio contexto de alcance local, y los cambios en la variable no se verán fuera del ciclo. El ciclo while es esencialmente un subshell que obtiene una COPIA del entorno del shell, y cualquier cambio en el entorno se pierde al final del shell. Vea esta pregunta de StackOverflow .

ACTUALIZADO : Me olvidé de señalar el hecho importante de que el ciclo while con su propio subshell se debía a que era el punto final de una tubería, lo actualicé en la respuesta.

jsbillings
fuente
@jsbillings .. Bien, eso explica los dos últimos fragmentos, pero no explica el primero, donde el valor de $ x establecido en el bucle, se lleva a cabo como 55 (más allá del alcance del bucle 'while')
Peter.O
55
@ fred.bear: está ejecutando el whilebucle como el extremo final de una tubería que lo arroja a una subshell.
geekosaur
2
Aquí es donde entra en juego la sustitución del proceso bash. En lugar de blah|blah|while read ..., puedes tenerwhile read ...; done < <(blah|blah)
glenn jackman
1
@geekosaur: gracias por completar los detalles que olvidé incluir en mi respuesta.
jsbillings
1
-1 Lo siento, pero esta respuesta es incorrecta. Explica cómo funciona esto en muchos lenguajes de programación pero no en el shell. @Gilles, abajo, lo hizo bien.
jpc
6

Como se mencionó en otras respuestas , las partes de una tubería se ejecutan en subcapas, por lo que las modificaciones realizadas allí no son visibles para el shell principal.

Si consideramos solo Bash, hay otras dos soluciones además de la cmd | { stuff; more stuff; }estructura:

  1. Redireccionar la entrada de la sustitución del proceso :

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    La salida del comando en <(...)se hace aparecer como si fuera una tubería con nombre.

  2. La lastpipeopción, que hace que Bash funcione como ksh, y ejecuta la última parte de la tubería en el proceso de shell principal. Aunque solo funciona si el control de trabajo está desactivado, es decir, no en un shell interactivo:

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    o

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

Por supuesto, la sustitución de procesos también es compatible con ksh y zsh. Pero dado que ejecutan la última parte de la tubería en el shell principal de todos modos, usarlo como una solución no es realmente necesario.

ilkkachu
fuente
0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

puede funcionar

GPS
fuente