¿Cómo puedo leer línea por línea de una variable en bash?

Tengo una variable que contiene la salida multilínea de un comando. ¿Cuál es la forma más eficaz de leer la salida línea por línea de la variable?

Por ejemplo:

jobs="$(jobs)" if [ "$jobs" ]; then # read lines from $jobs fi 

Respuesta

Puede utilizar un ciclo while con sustitución de procesos:

while read -r line do echo "$line" done < <(jobs) 

Una forma óptima de leer una variable de varias líneas es establecer una IFS variable en blanco y printf la variable con una nueva línea al final:

# Printf "%s\n" "$var" is necessary because printf "%s" "$var" on a # variable that doesn"t end with a newline then the while loop will # completely miss the last line of the variable. while IFS= read -r line do echo "$line" done < <(printf "%s\n" "$var") 

Nota: De acuerdo con shellcheck sc2031 , el uso de la sustitución de procesos es preferible a una tubería para evitar [sutilmente] creando un subshell.

Además, tenga en cuenta que al nombrar la variable jobs puede causar confusión ya que ese es también el nombre de un comando de shell común.

Comentarios

  • Si desea conservar todos los espacios en blanco, utilice while IFS= read …. Si quiere evitar \ interpretación, luego use read -r
  • Yo ‘ he arreglado los puntos que mencionó fred.bear, y también cambié echo a printf %s, para que su script funcione incluso con una entrada no domesticada.
  • Para leer desde una variable de varias líneas, es preferible una herestring a una canalización desde printf (consulte l0b0 ‘ s respuesta).
  • @ata Aunque ‘ he escuchado esto » preferible » con suficiente frecuencia, debe tenerse en cuenta que una cadena de caracteres siempre requiere que el directorio /tmp sea de escritura, ya que depende de que sea capaz de crear un archivo de trabajo temporal. Si alguna vez se encuentra en un sistema restringido con /tmp de solo lectura (y usted no puede cambiarlo), estará feliz con la posibilidad de usar una solución alternativa, e. gramo. con el printf pipe.
  • En el segundo ejemplo, si la variable de varias líneas no ‘ t contiene un nueva línea final perderá el último elemento. Cámbielo a: printf "%s\n" "$var" | while IFS= read -r line

Respuesta

Para procesar el salida de un comando línea por línea ( explicación ):

jobs | while IFS= read -r line; do process "$line" done 

Si tiene el datos ya en una variable:

printf %s "$foo" | … 

printf %s "$foo" es casi idéntico a echo "$foo", pero imprime $foo literalmente, mientras que echo "$foo" podría interpretar $foo como una opción al comando echo si comienza con un -, y puede expandir las secuencias de barra invertida en $foo en algunos shells.

Tenga en cuenta que en algunos shells (ash, bash, pdksh, pero no ksh o zsh), el lado derecho de una tubería se ejecuta en un proceso separado, por lo que cualquier variable que establezca en el ciclo se pierde. Por ejemplo, el siguiente script de conteo de líneas imprime 0 en estos shells:

n=0 printf %s "$foo" | while IFS= read -r line; do n=$(($n + 1)) done echo $n 

Una solución es poner el resto del script (o al menos la parte que necesita el valor de $n del ciclo) en una lista de comandos:

n=0 printf %s "$foo" | { while IFS= read -r line; do n=$(($n + 1)) done echo $n } 

Si actuar en las líneas no vacías es lo suficientemente bueno y la entrada no es enorme, puede usar la división de palabras:

IFS=" " set -f for line in $(jobs); do # process line done set +f unset IFS 

Explicación: configuración IFS a una sola línea nueva hace que la división de palabras ocurra solo en líneas nuevas (a diferencia de cualquier carácter de espacio en blanco en la configuración predeterminada). set -f desactiva el globbing (es decir, la expansión de comodines), que de otro modo ocurriría con el resultado de una sustitución de comando $(jobs) o una sustitución de variable $foo. El bucle for actúa sobre todas las piezas de $(jobs), que son todas las líneas no vacías en la salida del comando. Finalmente, restaure la configuración global y IFS a valores que sean equivalentes a los valores predeterminados.

Comentarios

  • He tenido problemas para configurar IFS y desarmar IFS. Creo que lo correcto es almacenar el valor anterior de IFS y restablecer IFS a ese valor anterior. ‘ no soy un experto en bash, pero en mi experiencia, esto te lleva de vuelta al comportamiento original.
  • @BjornRoche: dentro de una función, usa local IFS=something. Ganó ‘ t afectar el valor de alcance global. IIRC, unset IFS no ‘ no te devuelve al valor predeterminado (y ciertamente no ‘ no funciona si no era ‘ t el predeterminado de antemano).
  • Me pregunto si usar set en el camino mostrado en el último ejemplo es correcto.El fragmento de código asume que set +f estaba activo al principio y, por lo tanto, restaura esa configuración al final. Sin embargo, esta suposición puede ser incorrecta. ¿Qué pasa si set -f estaba activo al principio?
  • @Binarus Solo restauro configuraciones equivalentes a las predeterminadas. De hecho, si desea restaurar la configuración original, debe trabajar más. Para set -f, guarde el $- original. Para IFS, es ‘ molesto si no ‘ no tiene local y desea admitir el caso no establecido; si desea restaurarlo, le recomiendo aplicar el invariante que IFS permanece establecido.
  • El uso de local de hecho, será la mejor solución, porque local - hace que las opciones de shell sean locales, y local IFS hace que IFS local. Desafortunadamente, local solo es válido dentro de las funciones, lo que hace necesaria la reestructuración del código. Su sugerencia de introducir la política de que IFS siempre se establece también suena muy razonable y resuelve la mayor parte del problema. ¡Gracias!

Respuesta

Problema: si usa el bucle while, se ejecutará en una subcapa y todas las variables perdió. Solución: use for loop

# change delimiter (IFS) to new line. IFS_BAK=$IFS IFS=$"\n" for line in $variableWithSeveralLines; do echo "$line" # return IFS back if you need to split new line by spaces: IFS=$IFS_BAK IFS_BAK= lineConvertedToArraySplittedBySpaces=( $line ) echo "{lineConvertedToArraySplittedBySpaces[0]}" # return IFS back to newline for "for" loop IFS_BAK=$IFS IFS=$"\n" done # return delimiter to previous value IFS=$IFS_BAK IFS_BAK= 

Comentarios

  • ¡¡MUCHAS GRACIAS !! Todas las soluciones anteriores fallaron para mí.
  • La conexión a un bucle while read en bash significa que el bucle while está en una subcapa, por lo que las variables no son ‘ t global. while read;do ;done <<< "$var" hace que el cuerpo del bucle no sea una subcapa. (El bash reciente tiene una opción para poner el cuerpo de un bucle cmd | while no en un subshell, como siempre lo ha hecho ksh).
  • También vea esta publicación relacionada .
  • En situaciones similares, me resultó sorprendentemente difícil tratar IFS correctamente. Esta solución también tiene un problema: ¿Qué pasa si IFS no está configurado en absoluto al principio (es decir, no está definido)? Se definirá en todos los casos después de ese fragmento de código; esto no ‘ parece ser correcto.

Responder

jobs="$(jobs)" while IFS= read -r do echo "$REPLY" done <<< "$jobs" 

Referencias:

Comentarios

  • -r también es una buena idea; Evita \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(que es esencial para evitar perder espacios en blanco)
  • Solo esta solución funcionó para mí. Gracias, brah.
  • ¿No ‘ t esta solución tiene el mismo problema que se menciona en los comentarios a @dogbane ‘ s respuesta? ¿Qué pasa si la última línea de la variable no termina con un carácter de nueva línea?
  • Esta respuesta proporciona la forma más clara de alimentar el contenido de una variable al while read construir.

Respuesta

En versiones recientes de bash, use mapfile o readarray para leer eficientemente la salida del comando en matrices

$ readarray test < <(ls -ltrR) $ echo ${#test[@]} 6305 

Descargo de responsabilidad: ejemplo horrible, pero puede venir con un comando mejor para usar que ls usted mismo

Comentarios

  • Es ‘ una manera agradable, pero litters / var / tmp con archivos temporales en mi sistema. +1 de todos modos
  • @eugene: eso ‘ es gracioso. ¿En qué sistema (distribución / SO) está eso?
  • Es ‘ s FreeBSD 8. Cómo reproducirlo: ponga readarray en una función y llame a la función unas cuantas veces.
  • Bonito, @sehe. +1

Respuesta

Los patrones comunes para resolver este problema se han dado en las otras respuestas.

Sin embargo, me gustaría agregar mi enfoque, aunque no estoy seguro de cuán eficiente es. Pero es (al menos para mí) bastante comprensible, no altera la variable original (todas las soluciones que usan read debe tener la variable en cuestión con una nueva línea al final y, por lo tanto, agregarla, lo que altera la variable), no crea subcapas (que hacen todas las soluciones basadas en tuberías), no se usa aquí -cadenas (que tienen sus propios problemas) y no utiliza la sustitución de procesos (nada en contra, pero a veces es un poco difícil de entender).

En realidad, no entiendo por qué » se utilizan muy raramente.Quizás no sean portátiles, pero dado que el OP ha utilizado la etiqueta bash, eso «no me detendrá 🙂

#!/bin/bash function ProcessLine() { printf "%s" "$1" } function ProcessText1() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done ProcessLine "$Text" } function ProcessText2() { local Text=$1 local Pattern=$"^([^\n]*\n)(.*)$" while [[ "$Text" =~ $Pattern ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } function ProcessText3() { local Text=$1 local Pattern=$"^([^\n]*\n?)(.*)$" while [[ ("$Text" != "") && ("$Text" =~ $Pattern) ]]; do ProcessLine "${BASH_REMATCH[1]}" Text="${BASH_REMATCH[2]}" done } MyVar1=$"a1\nb1\nc1\n" MyVar2=$"a2\n\nb2\nc2" MyVar3=$"a3\nb3\nc3" ProcessText1 "$MyVar1" ProcessText1 "$MyVar2" ProcessText1 "$MyVar3" 

Salida:

root@cerberus:~/scripts# ./test4 a1 b1 c1 a2 b2 c2a3 b3 c3root@cerberus:~/scripts# 

Algunas notas:

El comportamiento depende de la variante de ProcessText que usa. En el ejemplo anterior, he usado ProcessText1.

Tenga en cuenta que

  • ProcessText1 mantiene los caracteres de nueva línea al final de las líneas
  • ProcessText1 procesa la última línea de la variable (que contiene el texto c3) aunque esa línea no contiene un carácter de nueva línea al final. Debido a la falta de una nueva línea al final, el símbolo del sistema después de la ejecución del script se agrega al último línea de la variable sin estar separada de la salida.
  • ProcessText1 siempre considera la parte entre la última línea nueva en la variable y el final de la variable como una línea , incluso si está vacío; por supuesto, esa línea, ya sea vacía o no, no tiene un carácter de nueva línea al final. Es decir, incluso si el último carácter de la variable es una nueva línea, ProcessText1 tratará la parte vacía (cadena nula) entre esa última línea nueva y el final de la variable como un (aún vacío) línea y lo pasará al procesamiento de línea. Puede prevenir fácilmente este comportamiento encapsulando la segunda llamada a ProcessLine en una condición apropiada de verificación si está vacía; sin embargo, creo que es más lógico dejarlo como está.

ProcessText1 necesita llamar a ProcessLine en dos lugares, lo que puede resultar incómodo si desea colocar un bloque de código allí que procesa directamente la línea, en lugar de llamar a una función que procesa la línea; tendría que repetir el código que es propenso a errores.

Por el contrario, ProcessText3 procesa la línea o llama a la función respectiva solo en un lugar, lo que hace que el reemplazo la llamada a la función por un bloque de código es una obviedad. Esto tiene el costo de dos while condiciones en lugar de una. Aparte de las diferencias de implementación, ProcessText3 se comporta exactamente igual que ProcessText1, excepto que no considera la parte entre el último carácter de nueva línea en la variable y el final de la variable como línea si esa parte está vacía. Es decir, ProcessText3 no pasará al procesamiento de línea después del último carácter de nueva línea de la variable si ese carácter de nueva línea es el último carácter de la variable.

ProcessText2 funciona como ProcessText1, excepto que las líneas deben tener un carácter de nueva línea al final. Es decir, la parte entre el último carácter de nueva línea de la variable y el final de la variable no se considera una línea y se desecha silenciosamente. En consecuencia, si la variable no contiene ningún carácter de nueva línea, no ocurre ningún procesamiento de línea.

Me gusta ese enfoque más que las otras soluciones mostradas arriba, pero probablemente me he perdido algo (no tengo mucha experiencia en bash programar, y no estar muy interesado en otros shells).

Responder

Puede usar < < < para leer simplemente desde la variable que contiene la nueva línea -datos separados:

while read -r line do echo "A line of input: $line" done <<<"$lines" 

Comentarios

  • Bienvenido a Unix & Linux! Esto esencialmente duplica una respuesta de hace cuatro años. No publique una respuesta a menos que tenga algo nuevo para contribuir.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *