Recorriendo el contenido de un archivo en Bash

1390

¿Cómo itero a través de cada línea de un archivo de texto con Bash ?

Con este script:

echo "Start!"
for p in (peptides.txt)
do
    echo "${p}"
done

Me sale este resultado en la pantalla:

Start!
./runPep.sh: line 3: syntax error near unexpected token `('
./runPep.sh: line 3: `for p in (peptides.txt)'

(Más tarde, quiero hacer algo más complicado $pque simplemente enviarlo a la pantalla).


La variable de entorno SHELL es (de env):

SHELL=/bin/bash

/bin/bash --version salida:

GNU bash, version 3.1.17(1)-release (x86_64-suse-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.

cat /proc/version salida:

Linux version 2.6.18.2-34-default (geeko@buildhost) (gcc version 4.1.2 20061115 (prerelease) (SUSE Linux)) #1 SMP Mon Nov 27 11:46:27 UTC 2006

El archivo peptides.txt contiene:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Peter Mortensen
fuente
19
Ah, veo que han sucedido muchas cosas aquí: se eliminaron todos los comentarios y se volvió a abrir la pregunta. Solo como referencia, la respuesta aceptada en Leer un archivo línea por línea asignando el valor a una variable aborda el problema de una manera canónica y debe preferirse sobre la aceptada aquí.
Fedorqui 'así que deja de dañar'

Respuestas:

2098

Una forma de hacerlo es:

while read p; do
  echo "$p"
done <peptides.txt

Como se señaló en los comentarios, esto tiene los efectos secundarios de recortar los espacios en blanco iniciales, interpretar las secuencias de barra invertida y omitir la última línea si falta un salto de línea de terminación. Si estas son preocupaciones, puede hacer:

while IFS="" read -r p || [ -n "$p" ]
do
  printf '%s\n' "$p"
done < peptides.txt

Excepcionalmente, si el cuerpo del bucle puede leer desde la entrada estándar , puede abrir el archivo usando un descriptor de archivo diferente:

while read -u 10 p; do
  ...
done 10<peptides.txt

Aquí, 10 es solo un número arbitrario (diferente de 0, 1, 2).

Bruno De Fraine
fuente
77
¿Cómo debo interpretar la última línea? ¿El archivo peptides.txt se redirige a la entrada estándar y de alguna manera a todo el bloque while?
Peter Mortensen el
11
"Mover peptides.txt en este ciclo while, por lo que el comando 'leer' tiene algo que consumir". Mi método "cat" es similar, enviando la salida de un comando al bloque while para consumo mediante 'read', también, solo que lanza otro programa para hacer el trabajo.
Warren Young el
8
Este método parece omitir la última línea de un archivo.
xastor
55
¡¡Doble comillas !! echo "$ p" y el archivo ... créeme, te morderá si no lo haces. ¡LO SÉ! lol
Mike Q
55
Ambas versiones no pueden leer una línea final si no se termina con una nueva línea. Siempre usewhile read p || [[ -n $p ]]; do ...
dawg
449
cat peptides.txt | while read line 
do
   # do something with $line here
done

y la variante de una línea:

cat peptides.txt | while read line; do something_with_$line_here; done

Estas opciones omitirán la última línea del archivo si no hay avance de línea final.

Puede evitar esto de la siguiente manera:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done
Warren Young
fuente
68
En general, si está usando "gato" con un solo argumento, está haciendo algo mal (o subóptimo).
JesperE el
27
Sí, simplemente no es tan eficiente como el de Bruno, porque lanza otro programa innecesariamente. Si la eficiencia importa, hazlo a la manera de Bruno. Recuerdo mi camino porque puedes usarlo con otros comandos, donde la sintaxis "redirigir desde" no funciona.
Warren Young el
74
Hay otro problema más serio con esto: debido a que el ciclo while es parte de una tubería, se ejecuta en un subshell y, por lo tanto, cualquier variable establecida dentro del ciclo se pierde cuando sale (vea bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Esto puede ser muy molesto (dependiendo de lo que intentes hacer en el bucle).
Gordon Davisson, el
25
Utilizo "cat file |" como inicio de muchos de mis comandos simplemente porque a menudo hago prototipos con "head file |"
mat kelcey
62
Esto puede no ser tan eficiente, pero es mucho más legible que otras respuestas.
Savage Reader
145

Opción 1a: Bucle While: línea única a la vez: redirección de entrada

#!/bin/bash
filename='peptides.txt'
echo Start
while read p; do 
    echo $p
done < $filename

Opción 1b: Bucle While: línea única a la vez:
abra el archivo, lea desde un descriptor de archivo (en este caso, el descriptor de archivo # 4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done
Stan Graves
fuente
Para la opción 1b: ¿es necesario volver a cerrar el descriptor de archivo? Por ejemplo, el bucle podría ser un bucle interno.
Peter Mortensen el
3
El descriptor de archivo se limpiará con las salidas del proceso. Se puede hacer un cierre explícito para reutilizar el número fd. Para cerrar un archivo fd, use otro exec con la sintaxis &, así: exec 4 <& -
Stan Graves
1
Gracias por la Opción 2. Me encontré con grandes problemas con la Opción 1 porque necesitaba leer desde stdin dentro del bucle; en tal caso, la opción 1 no funcionará.
masgo
44
Debe señalar más claramente que la Opción 2 no se recomienda . @masgo La opción 1b debería funcionar en ese caso, y se puede combinar con la sintaxis de redireccionamiento de entrada de la Opción 1a reemplazándola done < $filenamepor done 4<$filename(que es útil si desea leer el nombre del archivo de un parámetro de comando, en cuyo caso puede simplemente reemplazarlo $filenamepor $1)
Egor Hans
Necesito recorrer el contenido del archivo como tail -n +2 myfile.txt | grep 'somepattern' | cut -f3, mientras ejecuto comandos ssh dentro del ciclo (consume stdin); La opción 2 aquí parece ser la única manera?
user5359531
85

Esto no es mejor que otras respuestas, pero es una forma más de hacer el trabajo en un archivo sin espacios (ver comentarios). Me parece que a menudo necesito frases para buscar en las listas de los archivos de texto sin el paso adicional de usar archivos de script separados.

for word in $(cat peptides.txt); do echo $word; done

Este formato me permite ponerlo todo en una línea de comandos. Cambie la porción "echo $ word" a lo que quiera y puede emitir múltiples comandos separados por punto y coma. El siguiente ejemplo utiliza los contenidos del archivo como argumentos en otros dos scripts que puede haber escrito.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done

O si tiene la intención de usar esto como un editor de flujo (aprender sed) puede volcar la salida a otro archivo de la siguiente manera.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done > outfile.txt

Los he usado como se escribió anteriormente porque he usado archivos de texto donde los he creado con una palabra por línea. (Ver comentarios) Si tiene espacios en los que no desea dividir sus palabras / líneas, se vuelve un poco más feo, pero el mismo comando aún funciona de la siguiente manera:

OLDIFS=$IFS; IFS=$'\n'; for line in $(cat peptides.txt); do cmd_a.sh $line; cmd_b.py $line; done > outfile.txt; IFS=$OLDIFS

Esto solo le dice al shell que se divida solo en nuevas líneas, no en espacios, luego regresa el entorno a lo que era anteriormente. En este punto, es posible que desee considerar poner todo en un script de shell en lugar de exprimirlo todo en una sola línea.

¡La mejor de las suertes!

mightypile
fuente
66
El bash $ (<peptides.txt) es quizás más elegante, pero aún así está mal, lo que Joao dijo correcto, está realizando la lógica de sustitución de comandos donde el espacio o la nueva línea son lo mismo. Si una línea tiene un espacio, el bucle ejecuta DOS VECES o más para esa línea. Por lo tanto, su código debería leer correctamente: por palabra en $ (<peptides.txt); hacer .... Si sabes con certeza que no hay espacios, entonces una línea es igual a una palabra y estás bien.
maxpolk
2
@ JoaoCosta, maxpolk: Buenos puntos que no había considerado. He editado la publicación original para reflejarlos. ¡Gracias!
mightypile
2
El uso forhace que los tokens / líneas de entrada estén sujetos a expansiones de shell, lo que generalmente no es deseable; intente esto: for l in $(echo '* b c'); do echo "[$l]"; donecomo verá, el *- aunque originalmente era un literal citado - se expande a los archivos en el directorio actual.
mklement0
2
@dblanchard: El último ejemplo, usando $ IFS, debería ignorar los espacios. ¿Has probado esa versión?
mightypile
44
La forma en que este comando se vuelve mucho más complejo a medida que se solucionan problemas cruciales, presenta muy bien por qué usar foriterar líneas de archivo es una mala idea. Además, el aspecto de expansión mencionado por @ mklement0 (a pesar de que eso probablemente se puede eludir al incluir comillas escapadas, lo que nuevamente hace que las cosas sean más complejas y menos legibles).
Egor Hans
69

Algunas cosas más no cubiertas por otras respuestas:

Leer desde un archivo delimitado

# ':' is the delimiter here, and there are three fields on each line in the file
# IFS set below is restricted to the context of `read`, it doesn't affect any other code
while IFS=: read -r field1 field2 field3; do
  # process the fields
  # if the line has less than three fields, the missing fields will be set to an empty string
  # if the line has more than three fields, `field3` will get all the values, including the third field plus the delimiter(s)
done < input.txt

Lectura de la salida de otro comando, usando la sustitución del proceso

while read -r line; do
  # process the line
done < <(command ...)

Este enfoque es mejor que command ... | while read -r line; do ...porque el ciclo while aquí se ejecuta en el shell actual en lugar de un subshell como en el caso de este último. Ver la publicación relacionada No se recuerda una variable modificada dentro de un ciclo while .

Lectura de una entrada delimitada nula, por ejemplo find ... -print0

while read -r -d '' line; do
  # logic
  # use a second 'read ... <<< "$line"' if we need to tokenize the line
done < <(find /path/to/dir -print0)

Lectura relacionada: BashFAQ / 020 - ¿Cómo puedo encontrar y manejar con seguridad los nombres de archivo que contienen líneas nuevas, espacios o ambos?

Leer desde más de un archivo a la vez

while read -u 3 -r line1 && read -u 4 -r line2; do
  # process the lines
  # note that the loop will end when we reach EOF on either of the files, because of the `&&`
done 3< input1.txt 4< input2.txt

Basado en la respuesta de @ chepner aquí :

-ues una extensión bash. Por compatibilidad POSIX, cada llamada se vería algo así read -r X <&3.

Lectura de un archivo completo en una matriz (versiones Bash anteriores a la 4)

while read -r line; do
    my_array+=("$line")
done < my_file

Si el archivo termina con una línea incompleta (falta una nueva línea al final), entonces:

while read -r line || [[ $line ]]; do
    my_array+=("$line")
done < my_file

Lectura de un archivo completo en una matriz (versiones Bash 4x y posteriores)

readarray -t my_array < my_file

o

mapfile -t my_array < my_file

Y entonces

for line in "${my_array[@]}"; do
  # process the lines
done

Artículos Relacionados:

codeforester
fuente
tenga en cuenta que en lugar de command < input_filename.txtsiempre puede hacer input_generating_command | commandocommand < <(input_generating_command)
masterxilo
1
Gracias por leer el archivo en la matriz. Exactamente lo que necesito, porque necesito que cada línea se analice dos veces, agregue nuevas variables, haga algunas validaciones, etc.
frank_108
45

Use un ciclo while, como este:

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

Notas:

  1. Si no configura IFScorrectamente, perderá la sangría.

  2. Casi siempre debe usar la opción -r con lectura.

  3. No lea líneas con for

Jahid
fuente
2
¿Por qué la -ropción?
David C. Rankin
2
@ DavidC.Rankin La opción -r evita la interpretación de la barra invertida. Note #2es un enlace donde se describe en detalle ...
Jahid
Combina esto con la opción "read -u" en otra respuesta y luego es perfecto.
Florin Andrei
@FlorinAndrei: El ejemplo anterior no necesita la -uopción, ¿estás hablando de otro ejemplo -u?
Jahid
Miré a través de sus enlaces y me sorprendió que no haya una respuesta que simplemente vincule su enlace en la Nota 2. Esa página proporciona todo lo que necesita saber sobre ese tema. ¿O se desalientan las respuestas de solo enlace o algo así?
Egor Hans
14

Supongamos que tiene este archivo:

$ cat /tmp/test.txt
Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR

Hay cuatro elementos que alterarán el significado de la salida del archivo leída por muchas soluciones de Bash:

  1. La línea en blanco 4;
  2. Espacios iniciales o finales en dos líneas;
  3. Mantener el significado de líneas individuales (es decir, cada línea es un registro);
  4. La línea 6 no terminó con un CR.

Si desea que el archivo de texto línea por línea incluya líneas en blanco y líneas de terminación sin CR, debe usar un bucle while y debe tener una prueba alternativa para la línea final.

Estos son los métodos que pueden cambiar el archivo (en comparación con lo que catdevuelve):

1) Pierda la última línea y los espacios iniciales y finales:

$ while read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'

(Si lo hace while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt, conserva los espacios iniciales y finales pero aún pierde la última línea si no termina con CR)

2) El uso de la sustitución de procesos con catwill lee todo el archivo de una vez y pierde el significado de las líneas individuales:

$ for p in "$(cat /tmp/test.txt)"; do printf "%s\n" "'$p'"; done
'Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR'

(Si se quita la "de $(cat /tmp/test.txt)leer el archivo de la palabra por palabra en lugar de un solo trago. También probablemente no lo que se pretende ...)


La forma más sólida y sencilla de leer un archivo línea por línea y preservar todo el espaciado es:

$ while IFS= read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'    Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space    '
'Line 6 has no ending CR'

Si desea eliminar los espacios iniciales y comerciales, elimine la IFS=parte:

$ while read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'
'Line 6 has no ending CR'

(Un archivo de texto sin una terminación \n, aunque bastante común, se considera roto bajo POSIX. Si puede contar con el seguimiento \nno necesita || [[ -n $line ]]en el whilebucle).

Más en las preguntas frecuentes de BASH

perro
fuente
13

Si no desea que su lectura se rompa por el carácter de nueva línea, use -

#!/bin/bash
while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "$line"
done < "$1"

Luego ejecute el script con el nombre del archivo como parámetro.

Anjul Sharma
fuente
4
#!/bin/bash
#
# Change the file name from "test" to desired input file 
# (The comments in bash are prefixed with #'s)
for x in $(cat test.txt)
do
    echo $x
done
Seno
fuente
77
Esta respuesta necesita las advertencias mencionadas en la respuesta de mightypile , y puede fallar gravemente si alguna línea contiene metacaracteres de shell (debido al "$ x" sin comillas).
Toby Speight
77
De hecho, estoy sorprendido de que la gente aún no haya llegado a lo habitual . No lea líneas con ...
Egor Hans
3

Aquí está mi ejemplo de la vida real: cómo hacer un bucle en las líneas de otra salida del programa, verificar las subcadenas, descartar comillas dobles de la variable, usar esa variable fuera del bucle. Supongo que muchos hacen estas preguntas tarde o temprano.

##Parse FPS from first video stream, drop quotes from fps variable
## streams.stream.0.codec_type="video"
## streams.stream.0.r_frame_rate="24000/1001"
## streams.stream.0.avg_frame_rate="24000/1001"
FPS=unknown
while read -r line; do
  if [[ $FPS == "unknown" ]] && [[ $line == *".codec_type=\"video\""* ]]; then
    echo ParseFPS $line
    FPS=parse
  fi
  if [[ $FPS == "parse" ]] && [[ $line == *".r_frame_rate="* ]]; then
    echo ParseFPS $line
    FPS=${line##*=}
    FPS="${FPS%\"}"
    FPS="${FPS#\"}"
  fi
done <<< "$(ffprobe -v quiet -print_format flat -show_format -show_streams -i "$input")"
if [ "$FPS" == "unknown" ] || [ "$FPS" == "parse" ]; then 
  echo ParseFPS Unknown frame rate
fi
echo Found $FPS

Declarar variable fuera del ciclo, establecer el valor y usarlo fuera del ciclo requiere la sintaxis <<< "$ (...)" . La aplicación debe ejecutarse dentro de un contexto de la consola actual. Las comillas alrededor del comando mantienen nuevas líneas de flujo de salida.

La coincidencia de bucle para las subcadenas lee el nombre = par de valores , divide la parte derecha del último = carácter, suelta la primera comilla, suelta la última comilla, tenemos un valor limpio para usar en otro lugar.

Quien
fuente
3
Si bien la respuesta es correcta, entiendo cómo terminó aquí. El método esencial es el mismo que el propuesto por muchas otras respuestas. Además, se ahoga por completo en su ejemplo de FPS.
Egor Hans
0

Esto llega bastante tarde, pero con la idea de que puede ayudar a alguien, agrego la respuesta. Además, esta puede no ser la mejor manera. headEl comando se puede usar con -nargumentos para leer n líneas desde el inicio del archivo y del mismo modo tailse puede usar el comando para leer desde abajo. Ahora, para obtener la enésima línea del archivo, encabezamos n líneas , canalizamos los datos para seguir solo 1 línea de los datos canalizados.

   TOTAL_LINES=`wc -l $USER_FILE | cut -d " " -f1 `
   echo $TOTAL_LINES       # To validate total lines in the file

   for (( i=1 ; i <= $TOTAL_LINES; i++ ))
   do
      LINE=`head -n$i $USER_FILE | tail -n1`
      echo $LINE
   done
madD7
fuente
1
No hagas esto. Recorrer los números de línea y buscar cada línea individual por medio de sedo head+ tailes increíblemente ineficiente, y por supuesto plantea la pregunta de por qué no simplemente usa una de las otras soluciones aquí. Si necesita saber el número de línea, agregue un contador a su while read -rciclo o use nl -bapara agregar un prefijo de número de línea a cada línea antes del ciclo.
tripleee
0

Me gusta usar en xargslugar de while. xargses potente y amigable con la línea de comandos

cat peptides.txt | xargs -I % sh -c "echo %"

Con xargs, también puede agregar verbosidad -ty validación con-p

hamou92
fuente
-1

@ Peter: Esto podría funcionar para usted

echo "Start!";for p in $(cat ./pep); do
echo $p
done

Esto devolvería la salida

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Alan Jebakumar
fuente
11
¡Esto es muy malo! Por qué no lees líneas con "para" .
Fedorqui 'así que deja de dañar'
3
¡Esta respuesta está derrotando todos los principios establecidos por las buenas respuestas anteriores!
codeforester
3
Por favor borre esta respuesta.
dawg
3
Ahora chicos, no exageren. La respuesta es mala, pero parece funcionar, al menos para casos de uso simples. Siempre que se proporcione, ser una mala respuesta no quita el derecho de existir de la respuesta.
Egor Hans
3
@EgorHans, no estoy de acuerdo: el punto de las respuestas es enseñar a las personas cómo escribir software. Enseñar a las personas a hacer las cosas de una manera que usted sabe es perjudicial para ellas y las personas que usan su software (introduciendo errores / comportamientos inesperados / etc.) están dañando a otros a sabiendas. Una respuesta que se sabe que es dañina no tiene "derecho a existir" en un recurso de enseñanza bien curado (y curarlo es exactamente lo que nosotros, las personas que votamos y marcamos, se supone que debemos hacer aquí).
Charles Duffy