¿Cómo leer un archivo o STDIN en Bash?

245

El siguiente script de Perl ( my.pl) puede leer desde el archivo en la línea de comando args o desde STDIN:

while (<>) {
   print($_);
}

perl my.plleerá de STDIN, mientras perl my.pl a.txtque leerá de a.txt. Esto es muy conveniente.

¿Se pregunta si hay un equivalente en Bash?

Dagang
fuente

Respuestas:

410

La siguiente solución se lee desde un archivo si la secuencia de comandos se llama con un nombre de archivo como primer parámetro de lo $1contrario a partir de la entrada estándar.

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

La sustitución se ${1:-...}lleva a cabo $1si se define lo contrario, se utiliza el nombre de archivo de la entrada estándar del propio proceso.

Fritz G. Mehner
fuente
1
Bien, funciona. Otra pregunta es ¿por qué agrega una cotización? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang
15
El nombre de archivo que proporcione en la línea de comando podría tener espacios en blanco.
Fritz G. Mehner
3
¿Hay alguna diferencia entre usar /proc/$$/fd/0y /dev/stdin? Me di cuenta de que este último parece ser más común y parece más sencillo.
knowah
19
Es mejor agregar -ra su readcomando, para que no coma \ caracteres accidentalmente ; use while IFS= read -r linepara preservar los espacios en blanco iniciales y finales.
mklement0
1
@NeDark: Eso es curioso; Acabo de verificar que funciona en esa plataforma, incluso cuando se usa /bin/sh: ¿está utilizando un shell diferente de basho sh?
mklement0
119

Quizás la solución más simple es redirigir stdin con un operador de redirección de fusión:

#!/bin/bash
less <&0

Stdin es el descriptor de archivo cero. Lo anterior envía la entrada canalizada a su script bash en el stdin de less.

Lea más sobre la redirección del descriptor de archivo .

Ryan Ballantyne
fuente
1
Desearía tener más votos a favor para darte, he estado buscando esto durante años.
Marcus Downing
13
El uso <&0de esta situación no tiene ningún beneficio : su ejemplo funcionará igual con o sin él; aparentemente, las herramientas que invoca desde un script bash de manera predeterminada ven el mismo stdin que el script en sí (a menos que el script lo consuma primero).
mklement0
@ mkelement0 Entonces, si una herramienta lee la mitad del búfer de entrada, ¿la siguiente herramienta que invoco obtendrá el resto?
Asad Saeeduddin
"Falta el nombre de archivo (" less --help "para obtener ayuda)" cuando hago esto ... Ubuntu 16.04
OmarOthman
55
¿Dónde está la parte "o del archivo" en esta respuesta?
Sebastian
85

Aquí está la forma más simple:

#!/bin/sh
cat -

Uso:

$ echo test | sh my_script.sh
test

Para asignar stdin a la variable, puede usar: STDIN=$(cat -)o simplemente STDIN=$(cat)como operador no es necesario (según el comentario de @ mklement0 ).


Para analizar cada línea desde la entrada estándar , pruebe el siguiente script:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

Para leer del archivo o stdin (si el argumento no está presente), puede extenderlo a:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

Notas:

- read -r- No trate un carácter de barra invertida de ninguna manera especial. Considere que cada barra diagonal inversa sea parte de la línea de entrada.

- Sin configuración IFS, por defecto las secuencias de Spacey Tabal principio y al final de las líneas se ignoran (recortan).

- Use en printflugar de echopara evitar imprimir líneas vacías cuando la línea consiste en una sola -e, -no -E. Sin embargo, existe una solución mediante el uso env POSIXLY_CORRECT=1 echo "$line"que ejecuta su GNU externoecho que lo admite. Ver: ¿Cómo hago eco de "-e"?

Ver: ¿Cómo leer stdin cuando no se pasan argumentos? en stackoverflow SE

kenorb
fuente
Se podría simplificar [ "$1" ] && FILE=$1 || FILE="-"a FILE=${1:--}. (Quibble: mejor para evitar todas-mayúsculas cáscara variables en colisiones de nombres Evitar en caso de ambiente variables.)
mklement0
El gusto es mio; en realidad, ${1:--} es compatible con POSIX, por lo que debería funcionar en todos los shells similares a POSIX. Lo que no funcionará en todos estos shells es la sustitución de procesos ( <(...)); funcionará en bash, ksh, zsh, pero no en el tablero, por ejemplo. Además, es mejor agregar -ra su readcomando, para que no coma \ caracteres accidentalmente ; anteponer IFS= para preservar los espacios en blanco iniciales y finales.
mklement0
44
De hecho, su código aún se rompe debido a echo: si una línea consiste en -e, -no -E, no se mostrará. Para solucionar este problema, debe utilizar printf: printf '%s\n' "$line". No lo incluí en mi edición anterior ... con demasiada frecuencia mis ediciones se revierten cuando soluciono este error :(.
gniourf_gniourf
1
No, no falla. Y el --es inútil si el primer argumento es'%s\n'
gniourf_gniourf
1
Tu respuesta está bien para mí (quiero decir que ya no tengo errores ni características no deseadas), aunque no trata múltiples argumentos como lo hace Perl. De hecho, si desea manejar múltiples argumentos, terminará escribiendo la excelente respuesta de Jonathan Leffler; de hecho, la suya sería mejor ya que la usaría IFS=con ready en printflugar de echo. :).
gniourf_gniourf
19

Creo que este es el camino directo:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5
Amir Mehler
fuente
44
Esto no se ajusta al requisito del póster para leer desde stdin o un argumento de archivo, solo se lee desde stdin.
nash
3
Dejando @ objeción válida de Nash a un lado: readse lee de la entrada estándar por defecto , por lo que no hay necesidad de < /dev/stdin.
mklement0
13

La echosolución agrega nuevas líneas cada vez que IFSrompe el flujo de entrada. La respuesta de @ fgm se puede modificar un poco:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
David Souther
fuente
¿Podría explicar qué quiere decir con "la solución echo agrega nuevas líneas cada vez que IFS interrumpe el flujo de entrada"? En caso de que se está refiriendo a read's comportamiento: mientras read no potencialmente dividido en varias fichas de los caracteres. contenido $IFS, solo devuelve un token único si solo especifica un nombre de variable único (pero recorta y espacios en blanco iniciales y finales de forma predeterminada).
mklement0
@ mklement0 Estoy 100% de acuerdo con usted en el comportamiento de - ready agrega nuevas líneas sin la bandera. "La utilidad echo escribe los operandos especificados, separados por caracteres en blanco (` ') y seguidos por un carácter de nueva línea (`\ n'), en la salida estándar". $IFSecho-n
David Souther
Entendido. Sin embargo, para emular el bucle de Perl necesita el seguimiento \nagregado por echo: Perl $_ incluye la línea que termina \ndesde la línea leída, mientras que bash readno. (Sin embargo, como @gniourf_gniourf señala en otra parte, el enfoque más robusto es usar printf '%s\n'en lugar de echo).
mklement0
8

El bucle Perl en la pregunta se lee de todos los argumentos de nombre de archivo en la línea de comando, o de la entrada estándar si no se especifican archivos. Las respuestas que veo parecen procesar un solo archivo o entrada estándar si no se especifica ningún archivo.

Aunque a menudo se ridiculiza con precisión como UUOC (uso inútil de cat), hay momentos en que cates la mejor herramienta para el trabajo, y es discutible que esta sea una de ellas:

cat "$@" |
while read -r line
do
    echo "$line"
done

El único inconveniente de esto es que crea una tubería que se ejecuta en un sub-shell, por lo que cosas como asignaciones de variables en el whilebucle no son accesibles fuera de la tubería. La bashforma de evitarlo es la sustitución de procesos :

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

Esto deja el whileciclo ejecutándose en el shell principal, por lo que las variables establecidas en el ciclo son accesibles fuera del ciclo.

Jonathan Leffler
fuente
1
Excelente punto sobre múltiples archivos. No sé cuáles serían las implicaciones de recursos y rendimiento, pero si no está en bash, ksh o zsh y, por lo tanto, no puede usar la sustitución de procesos, puede probar un documento aquí con sustitución de comandos (distribuido en 3 líneas) >>EOF\n$(cat "$@")\nEOF. Finalmente, una objeción: while IFS= read -r linees una mejor aproximación de lo que while (<>)hace en Perl (conserva los espacios en blanco iniciales y finales, aunque Perl también mantiene el final \n).
mklement0
4

El comportamiento de Perl, con el código dado en el OP, puede tomar ninguno o varios argumentos, y si un argumento es un guión simple, -esto se entiende como stdin. Además, siempre es posible tener el nombre de archivo con $ARGV. Ninguna de las respuestas dadas hasta ahora realmente imita el comportamiento de Perl en estos aspectos. Aquí hay una posibilidad pura de Bash. El truco es usarlo execapropiadamente.

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

Nombre de archivo disponible en $1.

Si no se dan argumentos, los establecemos artificialmente -como el primer parámetro posicional. Luego hacemos un bucle en los parámetros. Si un parámetro no es -, redirigimos la entrada estándar del nombre del archivo con exec. Si esta redirección tiene éxito, hacemos un bucle con un whilebucle. Estoy usando la REPLYvariable estándar , y en este caso no es necesario reiniciar IFS. Si desea otro nombre, debe restablecerlo IFSasí (a menos que, por supuesto, no lo desee y sepa lo que está haciendo):

while IFS= read -r line; do
    printf '%s\n' "$line"
done
gniourf_gniourf
fuente
2

Con más precisión...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
sorigal
fuente
2
Supongo que esto es esencialmente un comentario en stackoverflow.com/a/6980232/45375 , no una respuesta. Para hacer que el comentario sea explícito: agregar IFS=y -r al readcomando asegura que cada línea se lea sin modificaciones (incluidos los espacios en blanco iniciales y finales).
mklement0
2

Por favor, intente el siguiente código:

while IFS= read -r line; do
    echo "$line"
done < file
Entusiasta de la web
fuente
1
Tenga en cuenta que incluso en su versión modificada, esto no se leerá de la entrada estándar o de varios archivos, por lo que no es una respuesta completa a la pregunta. (También es sorprendente ver dos ediciones en cuestión de minutos más de 3 años después de que se envió la respuesta por primera vez)
Jonathan Leffler
@JonathanLeffler, perdón por editar una respuesta tan antigua (y no muy buena) ... pero no podía soportar ver a este pobre readsin IFS=y -r, y al pobre $linesin sus citas saludables.
gniourf_gniourf
1
@gniourf_gniourf: no me gusta la read -rnotación. OMI, POSIX se equivocó; la opción debe habilitar el significado especial para las barras invertidas finales, no deshabilitarlo, de modo que los scripts existentes (anteriores a POSIX existieran) no se romperían porque -rse omitió. Sin embargo, observo que era parte de IEEE 1003.2 1992, que era la primera versión del estándar POSIX de shell y utilidades, pero se marcó como una adición incluso entonces, por lo que esto es una queja sobre las oportunidades perdidas. Nunca he tenido problemas porque mi código no se usa -r; Debo tener suerte. Ignórame sobre esto.
Jonathan Leffler
1
@ JonathanLeffler Realmente estoy de acuerdo en que -rdebería ser estándar. Estoy de acuerdo en que es poco probable que ocurra en los casos en que no usarlo ocasione problemas. Sin embargo, el código roto es un código roto. Mi edición se activó por primera vez por esa $linevariable pobre que perdió sus comillas. Lo arreglé readmientras estaba en eso. No arreglé el echoporque ese es el tipo de edición que se revierte. :(.
gniourf_gniourf
1

Code ${1:-/dev/stdin}solo entenderá el primer argumento, entonces, ¿qué tal esto?

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done
Takahiro Onodera
fuente
1

No encuentro ninguna de estas respuestas aceptables. En particular, la respuesta aceptada solo maneja el primer parámetro de línea de comando e ignora el resto. El programa Perl que está tratando de emular maneja todos los parámetros de la línea de comandos. Entonces la respuesta aceptada ni siquiera responde la pregunta. Otras respuestas usan extensiones bash, agregan comandos 'cat' innecesarios, solo funcionan para el caso simple de hacer eco de entrada a salida, o simplemente son innecesariamente complicados.

Sin embargo, tengo que darles algo de crédito porque me dieron algunas ideas. Aquí está la respuesta completa:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done
Gungwald
fuente
1

Combiné todas las respuestas anteriores y creé una función de shell que se adaptaría a mis necesidades. Esto es de un terminal cygwin de mis 2 máquinas con Windows10 donde tenía una carpeta compartida entre ellas. Necesito poder manejar lo siguiente:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

Cuando se especifica un nombre de archivo específico, necesito usar el mismo nombre de archivo durante la copia. Cuando se ha canalizado el flujo de datos de entrada, entonces necesito generar un nombre de archivo temporal que tenga la hora minuto y segundo. La carpeta principal compartida tiene subcarpetas de los días de la semana. Esto es para fines organizativos.

He aquí, el último guión para mis necesidades:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

Si hay alguna forma en que pueda ver para optimizar aún más esto, me gustaría saber.

truthadjustr
fuente
0

Lo siguiente funciona con estándar sh(Probado con dashDebian) y es bastante legible, pero eso es cuestión de gustos:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

Detalles: si el primer parámetro no está vacío, entonces catese archivo, de lo contrario cat, entrada estándar. Luego, la salida de toda la ifdeclaración es procesada por commands_and_transformations.

No en lista
fuente
En mi humilde opinión la mejor respuesta, así porque apunta a la verdadera solución: cat "${1:--}" | any_command. Leer a las variables de shell y hacer eco de ellas puede funcionar para archivos pequeños, pero no escala tan bien.
Andreas Spindler
El [ -n "$1" ]puede ser simplificado a [ "$1" ].
agc
0

Este es fácil de usar en la terminal:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3
cmcginty
fuente
-1

Qué tal si

for line in `cat`; do
    something($line);
done
Charles Cooper
fuente
La salida de catse colocará en la línea de comando. La línea de comando tiene un tamaño máximo. Además, esto no se leerá línea por línea, sino palabra por palabra.
Notinlist