Creando una matriz a partir de un archivo de texto en Bash

86

Un script toma una URL, la analiza en busca de los campos obligatorios y redirige su salida para guardarla en un archivo, file.txt . La salida se guarda en una nueva línea cada vez que se encuentra un campo.

file.txt

A Cat
A Dog
A Mouse 
etc... 

Quiero tomar file.txty crear una matriz a partir de ella en un nuevo script, donde cada línea llega a ser su propia variable de cadena en la matriz. Hasta ahora lo he intentado:

#!/bin/bash

filename=file.txt
declare -a myArray
myArray=(`cat "$filename"`)

for (( i = 0 ; i < 9 ; i++))
do
  echo "Element [$i]: ${myArray[$i]}"
done

Cuando ejecuto este script, los espacios en blanco dan como resultado que las palabras se dividan y en lugar de obtener

Salida deseada

Element [0]: A Cat 
Element [1]: A Dog 
etc... 

Termino recibiendo esto:

Salida real

Element [0]: A 
Element [1]: Cat 
Element [2]: A
Element [3]: Dog 
etc... 

¿Cómo puedo ajustar el bucle a continuación de modo que toda la cadena en cada línea corresponda uno a uno con cada variable en la matriz?

usuario2856414
fuente
5
De esto se trata Bash FAQ 001 . También esta sección del tema de la matriz en Bash FAQ 005 .
Etan Reisner
1
Vincularía esto como un duplicado de stackoverflow.com/questions/11393817/… , pero la respuesta aceptada es terrible.
Charles Duffy
Etan, ¡muchas gracias por una respuesta tan rápida y precisa! Intenté buscar mi pregunta en los foros, pero no pensé en buscar las preguntas frecuentes sobre stackoverflow. ¡El comando mapfile abordó mis necesidades exactamente! Gracias de nuevo :) Responda en la sección 2.1 .
user2856414
2
(Configure el enlace en la dirección opuesta, ya que tenemos una respuesta mejor aceptada aquí que allá).
Charles Duffy

Respuestas:

107

Usa el mapfilecomando:

mapfile -t myArray < file.txt

El error está usando for: la forma idiomática de recorrer las líneas de un archivo es:

while IFS= read -r line; do echo ">>$line<<"; done < file.txt

Consulte BashFAQ / 005 para obtener más detalles.

Glenn Jackman
fuente
5
Dado que este está siendo promovido como la canónica Q & A, también se puede incluir lo que se menciona en el enlace: while IFS= read -r; do lines+=("$REPLY"); done <file.
fedorqui 'SO deja de dañar'
10
mapfile no existe en versiones de bash anteriores a 4.x
ericslaw
14
Bash 4 tiene unos 5 años ahora. Potenciar.
Glenn Jackman
5
A pesar de que bash 4 se lanzó en 2009, el comentario de @ ericslaw sigue siendo relevante porque muchas máquinas todavía se envían con bash 3.x (y no se actualizarán, siempre que bash se publique bajo GPLv3). Si está interesado en la portabilidad, es importante tener en cuenta
De Novo
12
el problema no es que un desarrollador no pueda instalar una versión mejorada, es que un desarrollador debe ser consciente de que un script que usa mapfileno se ejecutará como se espera en muchas máquinas sin pasos adicionales. @ericslaw Mac continuará distribuyéndose con bash 3.2.57 en el futuro previsible. Las versiones más recientes usan una licencia que requeriría que Apple compartiera o permitiera cosas que no quieren compartir o permitir.
De Novo
23

mapfiley readarray(que son sinónimos) están disponibles en Bash versión 4 y superior. Si tiene una versión anterior de Bash, puede usar un bucle para leer el archivo en una matriz:

arr=()
while IFS= read -r line; do
  arr+=("$line")
done < file

En caso de que el archivo tenga una última línea incompleta (falta una línea nueva), puede usar esta alternativa:

arr=()
while IFS= read -r line || [[ "$line" ]]; do
  arr+=("$line")
done < file

Relacionado:

codeforester
fuente
Me parece que tengo que poner paréntesis IFS= read -r line || [[ "$line" ]]para que funcione. De lo contrario, ¡funciona muy bien!
Tatiana Racheva
@TatianaRacheva: ¿no es ese el punto y coma que faltaba antes do?
codeforester
9

Tu también puedes hacer esto:

oldIFS="$IFS"
IFS=$'\n' arr=($(<file))
IFS="$oldIFS"
echo "${arr[1]}" # It will print `A Dog`.

Nota:

La expansión del nombre de archivo todavía se produce. Por ejemplo, si hay una línea con un literal *, se expandirá a todos los archivos de la carpeta actual. Por lo tanto, utilícelo solo si su archivo está libre de este tipo de escenario.

Jahid
fuente
¿Hay alguna forma de establecer IFSsolo temporalmente (para que recupere su valor original después de este comando), mientras persiste la asignación arr?
Hugues
1
Tenga en cuenta que todavía se produce la expansión del nombre de archivo; Por ejemploIFS=$'\n' arr=($(echo 'a 1'; echo '*'; echo 'b 2')); printf "%s\n" "${arr[@]}"
Hugues
@Hugues: yap, la expansión del nombre de archivo todavía ocurre. Agregaré ese poco de información ... gracias ...
Jahid
Lo siento, no estoy de acuerdo. IFS=... commandno cambia IFSen el shell actual. Sin embargo, IFS=... other_variable=...(sin ningún comando) cambia ambos IFSy other_variableen el shell actual.
Hugues
1
¡Gracias! Esto funciona; Es lamentable que no haya una forma más sencilla, ya que me gusta la arr=notación (en comparación con mapfile/ readarray).
Hugues
4

Simplemente puede leer cada línea del archivo y asignarla a una matriz.

#!/bin/bash
i=0
while read line 
do
        arr[$i]="$line"
        i=$((i+1))
done < file.txt
Prateek Joshi
fuente
1
¿Cómo accedes a la matriz?
hola
4

Utilice mapfile o read -a

Siempre verifique su código usando shellcheck . A menudo le dará la respuesta correcta. En este caso, SC2207 cubre la lectura de un archivo que tiene valores separados por espacios o por líneas nuevas en una matriz.

No hagas esto

array=( $(mycommand) )

Archivos con valores separados por líneas nuevas

mapfile -t array < <(mycommand)

Archivos con valores separados por espacios

IFS=" " read -r -a array <<< "$(mycommand)"

La página de shellcheck le dará la razón por la que esto se considera la mejor práctica.

Cameron Lowell Palmer
fuente
0

Esta respuesta dice usar

mapfile -t myArray < file.txt

Hice una corrección para mapfilesi quieres usarla mapfileen bash <4.x por cualquier motivo. Utiliza el mapfilecomando existente si está en bash> = 4.x

Actualmente, solo opciones -dy -ttrabajo. Pero eso debería ser suficiente para el comando anterior. Solo lo he probado en macOS. En macOS Sierra 10.12.6, el bash del sistema es 3.2.57(1)-release. Así que la calza puede resultar útil. También puede actualizar su bash con homebrew, construir bash usted mismo, etc.

Utiliza esta técnica para configurar variables en una pila de llamadas.

dosisntmatter
fuente