No se recuerda una variable modificada dentro de un ciclo while


187

En el siguiente programa, si configuro la variable $fooen el valor 1 dentro de la primera ifinstrucción, funciona en el sentido de que su valor se recuerda después de la instrucción if. Sin embargo, cuando configuro la misma variable en el valor 2 dentro de un ifque está dentro de una whiledeclaración, se olvida después del whileciclo. Se comporta como si estuviera usando algún tipo de copia de la variable $foodentro del whilebucle y estoy modificando solo esa copia en particular. Aquí hay un programa 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)


La utilidad shellcheck lo detecta (consulte github.com/koalaman/shellcheck/wiki/SC2030 ); Cortar y pegar el código anterior en shellcheck.net emite estos comentarios para la Línea 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Respuestas:


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

los while bucle se ejecuta en una subshell. Por lo tanto, los cambios que realice en la variable no estarán disponibles una vez que la subshell salga.

En su lugar, puede usar una cadena here para volver a escribir el ciclo while para estar en el proceso de shell principal; solo echo -e $linesse 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")"

Puede deshacerse de lo bastante feo echoen la cadena de aquí arriba expandiendo las secuencias de barra invertida inmediatamente al asignar lines. La $'...'forma de cita se puede usar allí:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"

20
mejor cambio <<< "$(echo -e "$lines")"a simple<<< "$lines"
beliy

¿Qué pasa si la fuente era en tail -flugar de texto fijo?
mt eee

2
@mteee Puede usar while read -r line; do echo "LINE: $line"; done < <(tail -f file)(obviamente, el bucle no terminará ya que continúa esperando la entrada de tail).
PP

Estoy limitado a / bin / sh: ¿hay alguna forma alternativa que funcione en el antiguo shell?
user9645

1
@AvinashYadav El problema no está realmente relacionado con el whilebucle o el forbucle; más bien el uso de subshell, es decir, in cmd1 | cmd2, cmd2está en un subshell. Entonces, si forse ejecuta un bucle en una subshell, se mostrará el comportamiento inesperado / problemático.
PP

48

ACTUALIZADO # 2

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

Soluciones alternativas:

Eliminar echo

while read line; do
...
done <<EOT
first line
second line
third line
EOT

Agregue el eco dentro del documento aquí-es-el-documento

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

Ejecutar echoen segundo plano:

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

Redireccionar a un identificador de archivo explícitamente (¡Cuidado con el espacio < <!):

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

O simplemente redirigir a stdin:

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

Y uno para chepner(eliminar echo):

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

La variable $linesse puede convertir en una matriz sin iniciar un nuevo sub-shell. Los caracteres \y nse deben convertir a algún carácter (por ejemplo, un carácter de línea real nuevo) y utilizar la variable IFS (Separador de campo interno) 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")

+1 para el documento aquí, ya que el linesúnico propósito de la variable parece ser alimentar el whileciclo.
chepner

@chepner: ¡Gracias! ¡Agregué otro, dedicado a ti!
Verdaderamente

Hay otra solución dada aquí :for line in $(echo -e $lines); do ... done
dma_k

@dma_k ¡Gracias por tu comentario! Esta solución daría como resultado 6 líneas que contienen una sola palabra. La solicitud de OP fue diferente ...
TrueY

Votado Ejecutar echo en un subshell dentro de aquí es una de las pocas soluciones que funcionó en ceniza
Hamy

9

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

E4) Si canalizo la salida de un comando read variable, ¿por qué no aparece la salida $variablecuando 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 canalizaciones, no solo a llamadas simples read. Por ejemplo, canalizar la salida de un comando en un whilebucle que llama repetidamente readdará como resultado el mismo comportamiento.

Cada elemento de una tubería, incluso una función incorporada o shell, se ejecuta en un proceso separado, un elemento secundario de la tubería que ejecuta la tubería. Un subproceso no puede afectar el entorno de su padre. Cuando el readcomando establece la variable en la entrada, esa variable se establece solo en el subshell, no en el shell principal. Cuando la subshell sale, el valor de la variable se pierde.

Muchas canalizaciones que terminan con read variablepueden convertirse en sustituciones de comandos, que capturarán la salida de un comando específico. La salida 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 leer cuando se le dan argumentos de múltiples variables. Si necesita hacer esto, puede usar la sustitución de comandos anterior para leer el resultado en una variable y cortar la variable usando los operadores de expansión de eliminación de patrones bash o usar alguna variante del siguiente enfoque.

Digamos / 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 A B C D

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 hacerlo.

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

Algunas otras alternativas proporcionadas por el usuario incluyen:

read A B C D << HERE
    $(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE

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

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))

77
Olvidaste el problema de Family Feud . A veces es muy difícil encontrar la misma combinación de palabras que quien escribió la respuesta, de modo que no se vea inundado de resultados incorrectos ni filtre dicha respuesta.
Evi1M4chine

3

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

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

Cambie la primera línea del guión de:

#!/bin/bash

a

#!/bin/ksh

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


1

Utilizo stderr para almacenar dentro de un bucle, y leo desde afuera. Aquí var i se establece inicialmente y se lee dentro del bucle como 1.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value

f=/tmp/file1
g=/tmp/file2
i=1
cat $f $g | \
while read -r s;
do
  echo $s > /dev/null;  # some work
  echo $i > 2
  let i++
done;
read -r i < 2
echo $i

O utilice el método heredoc para reducir la cantidad de código en una subshell. Tenga en cuenta que el valor iterativo i se puede leer fuera del ciclo while.

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i

0

¿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.

66
Tal vez hay una respuesta decente debajo de la superficie aquí, pero has descifrado el formato hasta el punto de que es desagradable intentar leerlo.
Mark Amery

¿Quieres decir que el código original es agradable de leer? (Acabo de seguir: p)
Marcin

0

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 haciendo algún tipo de filtrado. Daré un ejemplo que puede ser útil en la vida real. Este es un fragmento para verificar que los archivos descargados se ajustan a una suma de verificación conocida. El archivo de suma de comprobación tiene el siguiente aspecto (mostrando 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 el subshell se comunican a través del comando echo. Puede elegir un texto fácil de analizar para el shell principal. Este método no rompe su forma normal de pensar, solo que tiene que hacer un procesamiento posterior. Puede usar grep, sed, awk y más para hacerlo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.