¿Por qué la entrada de tubería para “leer” solo funciona cuando se alimenta a la construcción “mientras se lee …”?

He intentado leer las entradas en las variables de entorno desde el resultado del progtwig de esta manera:

echo first second | read AB ; echo $A-$B 

Y el resultado es:

 - 

Tanto A como B están siempre vacíos. Leí acerca de bash ejecutando comandos de canalización en sub-shell y que básicamente impide que uno de los datos de entrada de la tubería sea leído. Sin embargo, lo siguiente:

 echo first second | while read AB ; do echo $A-$B ; done 

Parece que funciona, el resultado es:

 first-second 

¿Alguien puede explicar cuál es la lógica aquí? ¿Es que los comandos dentro de la construcción whiledone se ejecutan realmente en el mismo shell que echo y no en un subconjunto?

Cómo hacer un ciclo contra stdin y obtener el resultado almacenado en una variable

Bajo bash (y también otro shell ), cuando canalizas algo usando | para otro comando, creará implícitamente un fork , un subshell que es hijo de la sesión actual y que no puede afectar el entorno de la sesión actual.

Así que esto:

 TOTAL=0 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done echo final total: $TOTAL 

no dará el resultado esperado! :

  9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 echo final total: $TOTAL final total: 0 

Donde TOTAL calculado no puede ser reutilizado en el script principal.

Invertir la horquilla

Al usar Bash Process Substitution , Here Documents o Here Strings , puedes invertir el tenedor:

Aquí cuerdas

 read AB <<<"first second" echo $A first echo $B second 

Aquí documentos

 while read AB;do echo $A-$B C=$A-$B done << eodoc first second third fourth eodoc first-second third-fourth 

fuera del circuito:

 echo : $C : third-fourth 

Aquí los comandos

 TOTAL=0 while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done < <( printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 ) 9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 # and finally out of loop: echo $TOTAL -343 

Ahora podría usar $TOTAL en su secuencia de comandos principal .

Tubería a una lista de comandos

Pero para trabajar solo contra stdin , puedes crear un tipo de script en la bifurcación :

 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | { TOTAL=0 while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done echo "Out of the loop total:" $TOTAL } 

Daré:

  9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 Out of the loop total: -343 

Nota: $TOTAL no se pudo usar en el script principal (después del último corchete derecho } ).

Usar la opción lastpipe bash

Como @CharlesDuffy señaló correctamente, hay una opción bash utilizada para cambiar este comportamiento. Pero para esto, primero tenemos que desactivar el control de trabajo :

 shopt -s lastpipe # Set *lastpipe* option set +m # Disabling job control TOTAL=0 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done 9 - 4 = 5 -> TOTAL= -338 3 - 1 = 2 -> TOTAL= -336 77 - 2 = 75 -> TOTAL= -261 25 - 12 = 13 -> TOTAL= -248 226 - 664 = -438 -> TOTAL= -686 echo final total: $TOTAL -343 

Esto funcionará, pero a mí (personalmente) no me gusta esto porque esto no es estándar y no ayudará a que el guión sea legible. También el deshabilitar el control del trabajo parece costoso para acceder a este comportamiento.

Nota: El control de trabajos está habilitado de manera predeterminada solo en sesiones interactivas . Por lo tanto, no se requiere set +m en las secuencias de comandos normales.

Por lo tanto, el set +m olvidado en un script crearía diferentes comportamientos si se ejecuta en una consola o si se ejecuta en un script. Esto no va a hacer que esto sea fácil de entender o depurar ...

Primero, este pipe-chain se ejecuta:

 echo first second | read AB 

entonces

 echo $A-$B 

Debido a que la read AB se ejecuta en una subshell, A y B se pierden. Si haces esto:

 echo first second | (read AB ; echo $A-$B) 

luego ambos read AB y echo $A-$B se ejecutan en la misma subcadena (consulte la página de manual de bash, búsqueda de (list)

un trabajo mucho más limpio …

 read -rab < <(echo "$first $second") echo "$a $b" 

De esta forma, la lectura no se ejecuta en un subconjunto (lo que borraría las variables tan pronto como haya terminado ese subconjunto). En su lugar, las variables que desea utilizar se repiten en un subconjunto que hereda automáticamente las variables del shell primario.

Lo que está viendo es la separación entre procesos: la read produce en una subcapa, un proceso separado que no puede alterar las variables en el proceso principal (donde se producen los comandos de echo más adelante).

Una canalización (como A | B ) coloca implícitamente cada componente en un subconjunto (un proceso separado), incluso para los integrables (como read ) que generalmente se ejecutan en el contexto del shell (en el mismo proceso).

La diferencia en el caso de “conectar con el tiempo” es una ilusión. La misma regla se aplica allí: el bucle es la segunda mitad de una tubería, por lo que se encuentra en una subcadena, pero todo el bucle está en la misma subcadena, por lo que la separación de procesos no se aplica.