read - if bash
Una variable modificada dentro de un ciclo while no se recuerda (6)
En el siguiente programa, 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 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)
¿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.
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 sumas de comprobación. El archivo de suma 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.
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.
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 aread
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 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 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 A B C D << HERE $(IFS=.; echo $(/usr/local/bin/ipaddr)) HERE
y, cuando la sustitución del proceso está disponible,
read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
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 here-is-the-document
while read line; do
...
done <<EOT
$(echo -e $lines)
EOT
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")
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")"