Una variable modificada dentro de un ciclo while no se recuerda

En el siguiente progtwig, si configuro la variable $foo en el valor 1 dentro de la primera instrucción if , funciona en el sentido de que su valor se recuerda después de la instrucción if. Sin embargo, cuando configuro la misma variable para el valor 2 dentro de un if que está dentro de una sentencia while , se olvida después del ciclo while. Se está comportando como si estuviera usando algún tipo de copia de la variable $foo dentro del ciclo while y estoy modificando solo esa copia en particular. Aquí hay un progtwig de prueba completo:

 #!/bin/bash set -e set -u foo=0 bar="hello" if [[ "$bar" == "hello" ]] then foo=1 echo "Setting \$foo to 1: $foo" fi echo "Variable \$foo after if statement: $foo" lines="first line\nsecond line\nthird line" echo -e $lines | while read line do if [[ "$line" == "second line" ]] then foo=2 echo "Variable \$foo updated to $foo inside if inside while loop" fi echo "Value of \$foo in while loop body: $foo" done echo "Variable \$foo after while loop: $foo" # Output: # $ ./testbash.sh # Setting $foo to 1: 1 # Variable $foo after if statement: 1 # Value of $foo in while loop body: 1 # Variable $foo updated to 2 inside if inside while loop # Value of $foo in while loop body: 2 # Value of $foo in while loop body: 2 # Variable $foo after while loop: 1 # bash --version # GNU bash, version 4.1.10(4)-release (i686-pc-cygwin) 

 echo -e $lines | while read line ... done 

El ciclo while se ejecuta en una subcadena. Por lo tanto, cualquier cambio que realice en la variable no estará disponible una vez que la subshell finalice.

En su lugar, puede usar una cadena aquí para volver a escribir el ciclo while para estar en el proceso de shell principal; solo echo -e $lines se ejecutará en una subshell:

 while read line do if [[ "$line" == "second line" ]] then foo=2 echo "Variable \$foo updated to $foo inside if inside while loop" fi echo "Value of \$foo in while loop body: $foo" done < << "$(echo -e "$lines")" 

ACTUALIZADO # 2

La explicación está en la respuesta de Blue Moons.

Soluciones alternativas:

Eliminar echo

 while read line; do ... done <  

Agregue el eco dentro del here-is-the-document

 while read line; do ... done <  

Ejecutar echo en segundo plano:

 coproc echo -e $lines while read -u ${COPROC[0]} line; do ... done 

Redirija a un manejador de archivo explícitamente (¡Tenga en cuenta el espacio en < < !):

 exec 3< <(echo -e $lines) while read -u 3 line; do ... done 

O simplemente redirigir al stdin :

 while read line; do ... done < <(echo -e $lines) 

Y uno para chepner (eliminación de echo ):

 arr=("first line" "second line" "third line"); for((i=0;i< ${#arr[*]};++i)) { line=${arr[i]}; ... } 

Las $lines variables se pueden convertir a una matriz sin iniciar una nueva subcartera. Los caracteres \ n deben convertirse a algún carácter (por ejemplo, un nuevo carácter de línea real) y usar la variable IFS (Separador interno de campos) para dividir la cadena en elementos de la matriz. Esto se puede hacer como:

 lines="first line\nsecond line\nthird line" echo "$lines" OIFS="$IFS" IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion IFS="$OIFS" echo "${arr[@]}", Length: ${#arr[*]} set|grep ^arr 

El resultado es

 first line\nsecond line\nthird line first line second line third line, Length: 3 arr=([0]="first line" [1]="second line" [2]="third line") 

Usted es el usuario 742342º para hacer estas preguntas frecuentes sobre bash. La respuesta también describe el caso general de las variables establecidas en las subcapas creadas por las tuberías:

E4) Si canalizo la salida de un comando a la read variable , ¿por qué el resultado no se muestra en $variable cuando finaliza el comando de lectura?

Esto tiene que ver con la relación padre-hijo entre los procesos de Unix. Afecta a todos los comandos que se ejecutan en las tuberías, no solo llamadas simples para read . Por ejemplo, canalizar la salida de un comando en un ciclo while que llama repetidamente a read producirá el mismo comportamiento.

Cada elemento de una canalización, incluso una función incorporada o shell, se ejecuta en un proceso separado, un elemento secundario del shell que ejecuta la canalización. Un subproceso no puede afectar el entorno de sus padres. Cuando el comando de read establece la variable en la entrada, esa variable se establece solo en la subcadena, no en la capa principal. Cuando la subshell sale, el valor de la variable se pierde.

Muchas interconexiones que finalizan con la read variable se pueden convertir en sustituciones de comandos, que capturarán la salida de un comando especificado. El resultado se puede asignar a una variable:

 grep ^gnu /usr/lib/news/active | wc -l | read ngroup 

se puede convertir en

 ngroup=$(grep ^gnu /usr/lib/news/active | wc -l) 

Desafortunadamente, esto no funciona para dividir el texto entre múltiples variables, como lo hace la lectura cuando se le dan múltiples argumentos variables. Si necesita hacer esto, puede usar la sustitución de comando anterior para leer el resultado en una variable y cortar la variable utilizando los operadores de expansión de eliminación de patrón de bash o usar alguna variante del siguiente enfoque.

Say / usr / local / bin / ipaddr es el siguiente script de shell:

 #! /bin/sh host `hostname` | awk '/address/ {print $NF}' 

En lugar de usar

 /usr/local/bin/ipaddr | read ABCD 

para dividir la dirección IP de la máquina local en octetos separados, use

 OIFS="$IFS" IFS=. set -- $(/usr/local/bin/ipaddr) IFS="$OIFS" A="$1" B="$2" C="$3" D="$4" 

Sin embargo, tenga en cuenta que esto cambiará los parámetros posicionales del shell. Si los necesita, debe guardarlos antes de hacer esto.

Este es el enfoque general: en la mayoría de los casos, no necesitará establecer $ IFS en un valor diferente.

Algunas otras alternativas suministradas por el usuario incluyen:

 read ABCD < < HERE $(IFS=.; echo $(/usr/local/bin/ipaddr)) HERE 

y, cuando la sustitución del proceso está disponible,

 read ABCD < <(IFS=.; echo $(/usr/local/bin/ipaddr)) 

Hmmm … Casi juraría que esto funcionó para el shell original de Bourne, pero no tengo acceso a una copia en ejecución justo ahora para verificar.

Sin embargo, hay una solución muy trivial al problema.

Cambie la primera línea del script desde:

 #!/bin/bash 

a

 #!/bin/ksh 

Et voila! Una lectura al final de una tubería funciona bien, suponiendo que tiene instalado el shell Korn.

Esta es una pregunta interesante y toca un concepto muy básico en Bourne shell y subshell. Aquí proporciono una solución que es diferente de las soluciones anteriores al hacer algún tipo de filtrado. Daré un ejemplo que puede ser útil en la vida real. Este es un fragmento para verificar los archivos descargados y conocer las sums de comprobación. El archivo de sum de comprobación tiene el siguiente aspecto (muestra solo 3 líneas):

 49174 36326 dna_align_feature.txt.gz 54757 1 dna.txt.gz 55409 9971 exon_transcript.txt.gz 

El script de shell:

 #!/bin/sh ..... failcnt=0 # this variable is only valid in the parent shell #variable xx captures all the outputs from the while loop xx=$(cat ${checkfile} | while read -r line; do num1=$(echo $line | awk '{print $1}') num2=$(echo $line | awk '{print $2}') fname=$(echo $line | awk '{print $3}') if [ -f "$fname" ]; then res=$(sum $fname) filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}') if [ "$filegood" = "FALSE" ]; then failcnt=$(expr $failcnt + 1) # only in subshell echo "$fname BAD $failcnt" fi fi done | tail -1) # I am only interested in the final result # you can capture a whole bunch of texts and do further filtering failcnt=${xx#* BAD } # I am only interested in the number # this variable is in the parent shell echo failcnt $failcnt if [ $failcnt -gt 0 ]; then echo $failcnt files failed else echo download successful fi 

El padre y la subshell se comunican a través del comando echo. Puede elegir un texto fácil de analizar para el shell primario. Este método no rompe su forma normal de pensar, solo que tiene que hacer un poco de procesamiento posterior. Puede usar grep, sed, awk y más para hacerlo.

¿Qué tal un método muy simple?

  +call your while loop in a function - set your value inside (nonsense, but shows the example) - return your value inside +capture your value outside +set outside +display outside #!/bin/bash # set -e # set -u # No idea why you need this, not using here foo=0 bar="hello" if [[ "$bar" == "hello" ]] then foo=1 echo "Setting \$foo to $foo" fi echo "Variable \$foo after if statement: $foo" lines="first line\nsecond line\nthird line" function my_while_loop { echo -e $lines | while read line do if [[ "$line" == "second line" ]] then foo=2; return 2; echo "Variable \$foo updated to $foo inside if inside while loop" fi echo -e $lines | while read line do if [[ "$line" == "second line" ]] then foo=2; echo "Variable \$foo updated to $foo inside if inside while loop" return 2; fi # Code below won't be executed since we returned from function in 'if' statement # We aready reported the $foo var beint set to 2 anyway echo "Value of \$foo in while loop body: $foo" done } my_while_loop; foo="$?" echo "Variable \$foo after while loop: $foo" Output: Setting $foo 1 Variable $foo after if statement: 1 Value of $foo in while loop body: 1 Variable $foo after while loop: 2 bash --version GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13) Copyright (C) 2007 Free Software Foundation, Inc.