Comment lire à partir d'un fichier ou STDIN dans Bash?

246

Le script Perl ( my.pl) suivant peut lire à partir du fichier sur la ligne de commande args ou de STDIN:

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

perl my.pllira à partir de STDIN, tandis que perl my.pl a.txtlira à partir de a.txt. C'est très pratique.

Vous vous demandez s'il y a un équivalent dans Bash?

Dagang
la source

Réponses:

411

La solution suivante lit à partir d'un fichier si le script est appelé avec un nom de fichier comme premier paramètre $1sinon à partir de l'entrée standard.

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

La substitution ${1:-...}prend $1si définie sinon le nom de fichier de l'entrée standard du propre processus est utilisé.

Fritz G. Mehner
la source
1
Bien, ça marche. Une autre question est pourquoi vous ajoutez un devis pour cela? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang
15
Le nom de fichier que vous fournissez sur la ligne de commande peut contenir des espaces.
Fritz G. Mehner
3
Y a-t-il une différence entre utiliser /proc/$$/fd/0et /dev/stdin? J'ai remarqué que ce dernier semble être plus courant et semble plus simple.
knowah
19
Mieux vaut ajouter -rà votre readcommande, afin qu'elle ne mange pas accidentellement des \ caractères; utiliser while IFS= read -r linepour conserver les espaces de début et de fin.
mklement0
1
@NeDark: C'est curieux; Je viens de vérifier que cela fonctionne sur cette plate-forme, même lors de l'utilisation /bin/sh- utilisez -vous un shell autre que bashou sh?
mklement0
119

La solution la plus simple est peut-être de rediriger stdin avec un opérateur de redirection fusionné:

#!/bin/bash
less <&0

Stdin est le descripteur de fichier zéro. Ce qui précède envoie l'entrée redirigée vers votre script bash dans moins de stdin.

En savoir plus sur la redirection des descripteurs de fichiers .

Ryan Ballantyne
la source
1
J'aimerais avoir plus de votes positifs à vous donner, je le cherche depuis des années.
Marcus Downing
13
Il n'y a aucun avantage à utiliser <&0dans cette situation - votre exemple fonctionnera de la même manière avec ou sans lui - apparemment, les outils que vous invoquez à partir d'un script bash par défaut voient le même stdin que le script lui-même (sauf si le script le consomme en premier).
mklement0
@ mkelement0 Donc, si un outil lit la moitié du tampon d'entrée, le prochain outil que j'invoque obtiendra-t-il le reste?
Asad Saeeduddin
"Nom de fichier manquant (" moins --help "pour obtenir de l'aide)" quand je fais cela ... Ubuntu 16.04
OmarOthman
5
où est la partie "ou du fichier" dans cette réponse?
Sebastian
85

Voici le moyen le plus simple:

#!/bin/sh
cat -

Usage:

$ echo test | sh my_script.sh
test

Pour affecter stdin à la variable, vous pouvez utiliser: STDIN=$(cat -)ou tout simplement STDIN=$(cat)car l'opérateur n'est pas nécessaire (selon le commentaire @ mklement0 ).


Pour analyser chaque ligne à partir de l' entrée standard , essayez le script suivant:

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

Pour lire à partir du fichier ou de stdin (si aucun argument n'est présent), vous pouvez l'étendre à:

#!/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")

Remarques:

- read -r- Ne traitez pas un caractère barre oblique inverse d'une manière spéciale. Considérez chaque barre oblique inverse comme faisant partie de la ligne d'entrée.

- Sans réglage IFS, par défaut, les séquences de Spaceet Tabau début et à la fin des lignes sont ignorées (découpées).

- Utilisez printfau lieu de echopour éviter d'imprimer des lignes vides lorsque la ligne se compose d'un seul -e, -nou -E. Cependant, il existe une solution de contournement en utilisant env POSIXLY_CORRECT=1 echo "$line"qui exécute votre GNU externeecho qui le prend en charge. Voir: Comment puis-je faire écho à "-e"?

Voir: Comment lire stdin quand aucun argument n'est passé? à stackoverflow SE

Kenorb
la source
Vous pouvez simplifier [ "$1" ] && FILE=$1 || FILE="-"à FILE=${1:--}. (Quibble: mieux pour éviter les variables de shell tout en majuscules pour éviter les collisions de noms avec les variables d' environnement .)
mklement0
Mon plaisir; en fait, ${1:--} est compatible POSIX, donc il devrait fonctionner dans tous les shells de type POSIX. Ce qui ne fonctionnera pas dans tous ces shells, c'est la substitution de processus ( <(...)); cela fonctionnera en bash, ksh, zsh, mais pas en dash, par exemple. Aussi, mieux vaut ajouter -rà votre readcommande, afin qu'elle ne mange pas accidentellement des \ caractères; ajouter IFS= au début pour conserver les espaces de début et de fin.
mklement0
4
En fait, votre code se casse toujours à cause de echo: si une ligne se compose de -e, -nou -E, elle ne sera pas affichée. Pour résoudre ce problème, vous devez utiliser printf: printf '%s\n' "$line". Je ne l'ai pas inclus dans ma précédente édition… trop souvent mes modifications sont annulées lorsque je corrige cette erreur :(.
gniourf_gniourf
1
Non ça ne manque pas. Et le --est inutile si le premier argument est'%s\n'
gniourf_gniourf
1
Je réponds bien à votre réponse (je veux dire qu'il n'y a plus de bogues ou de fonctionnalités indésirables que je connaisse) - même si elle ne traite pas plusieurs arguments comme Perl. En fait, si vous voulez gérer plusieurs arguments, vous finirez par écrire l'excellente réponse de Jonathan Leffler - en fait, la vôtre serait meilleure puisque vous utiliseriez IFS=avec readet printfau lieu de echo. :).
gniourf_gniourf
19

Je pense que c'est la voie directe:

$ 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
la source
4
Cela ne correspond pas à l'exigence de l'affiche de lire à partir de stdin ou d'un argument de fichier, cela lit simplement à partir de stdin.
nash
3
Laissant de côté l'objection valide de @ nash: readlit à partir de stdin par défaut , donc il n'y a pas besoin de < /dev/stdin.
mklement0
13

La echosolution ajoute de nouvelles lignes chaque fois que IFSle flux d'entrée est interrompu. La réponse de @ fgm peut être modifiée un peu:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
David Souther
la source
Pourriez-vous expliquer ce que vous entendez par «la solution d'écho ajoute de nouvelles lignes chaque fois que IFS rompt le flux d'entrée»? Dans le cas où vous faisiez référence au readcomportement de ': while read est potentiellement divisé en plusieurs jetons par les caractères. contenu dans $IFS, il ne renvoie qu'un seul jeton si vous ne spécifiez qu'un seul nom de variable (mais les trims et les espaces de début et de fin par défaut).
mklement0
@ mklement0 Je suis d'accord à 100% avec vous sur le comportement de readet $IFS- echolui - même ajoute de nouvelles lignes sans le -ndrapeau. "L'utilitaire d'écho écrit tous les opérandes spécifiés, séparés par des caractères blancs ('') et suivis d'un caractère de nouvelle ligne (\ \ ') dans la sortie standard."
David Souther
Je l'ai. Cependant, pour émuler la boucle Perl, vous avez besoin de la fin \najoutée par echo: Perl $_ inclut la ligne se terminant à \npartir de la ligne lue, readcontrairement à bash . (Cependant, comme @gniourf_gniourf le fait remarquer ailleurs, l'approche la plus robuste consiste à utiliser printf '%s\n'à la place de echo).
mklement0
8

La boucle Perl dans la question lit à partir de tous les arguments de nom de fichier sur la ligne de commande, ou à partir de l'entrée standard si aucun fichier n'est spécifié. Les réponses que je vois semblent toutes traiter un seul fichier ou une entrée standard si aucun fichier n'est spécifié.

Bien que souvent ridiculisé avec précision comme UUOC (utilisation inutile de cat), il y a des moments où catest le meilleur outil pour le travail, et on peut soutenir que c'est l'un d'entre eux:

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

Le seul inconvénient est qu'il crée un pipeline s'exécutant dans un sous-shell, donc des choses comme les affectations de variables dans la whileboucle ne sont pas accessibles en dehors du pipeline. La bashsolution est la substitution de processus :

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

Cela laisse la whileboucle en cours d'exécution dans le shell principal, donc les variables définies dans la boucle sont accessibles en dehors de la boucle.

Jonathan Leffler
la source
1
Excellent point sur plusieurs fichiers. Je ne sais pas quelles seraient les implications en termes de ressources et de performances, mais si vous n'êtes pas sur bash, ksh ou zsh et que vous ne pouvez donc pas utiliser la substitution de processus, vous pouvez essayer un ici-doc avec substitution de commandes (réparti sur 3 lignes) >>EOF\n$(cat "$@")\nEOF. Enfin, un petit problème: while IFS= read -r lineest une meilleure approximation de ce qui se while (<>)passe en Perl (préserve les espaces de début et de fin - bien que Perl conserve également le dernier \n).
mklement0
4

Le comportement de Perl, avec le code donné dans l'OP, peut prendre aucun ou plusieurs arguments, et si un argument est un trait d'union, -cela est compris comme stdin. De plus, il est toujours possible d'avoir le nom de fichier avec $ARGV. Aucune des réponses données jusqu'à présent ne reproduit vraiment le comportement de Perl à cet égard. Voici une pure possibilité Bash. L'astuce consiste à utiliser de execmanière appropriée.

#!/bin/bash

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

Le nom de fichier est disponible en $1.

Si aucun argument n'est donné, nous le définissons artificiellement -comme premier paramètre positionnel. On boucle ensuite sur les paramètres. Si un paramètre ne l'est pas -, nous redirigeons l'entrée standard du nom de fichier avec exec. Si cette redirection réussit, nous bouclons avec une whileboucle. J'utilise la REPLYvariable standard , et dans ce cas, vous n'avez pas besoin de réinitialiser IFS. Si vous voulez un autre nom, vous devez le réinitialiser IFScomme ça (à moins, bien sûr, que vous ne le vouliez pas et que vous sachiez ce que vous faites):

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

Plus précisément...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
sorpigal
la source
2
Je suppose que c'est essentiellement un commentaire sur stackoverflow.com/a/6980232/45375 , pas une réponse. Pour rendre le commentaire explicite: l'ajout de IFS=et -r à la readcommande garantit que chaque ligne est lue sans modification (y compris les espaces de début et de fin).
mklement0
2

Veuillez essayer le code suivant:

while IFS= read -r line; do
    echo "$line"
done < file
Webthusiast
la source
1
Notez que même modifié, il ne sera pas lu à partir de l'entrée standard ou de plusieurs fichiers, donc ce n'est pas une réponse complète à la question. (Il est également surprenant de voir deux modifications en quelques minutes plus de 3 ans après la première soumission de la réponse.)
Jonathan Leffler
@JonathanLeffler désolé d'avoir édité une réponse aussi ancienne (et pas vraiment bonne)… mais je ne supportais pas de voir ce pauvre readsans IFS=et -r, et le pauvre $linesans ses citations saines.
gniourf_gniourf
1
@gniourf_gniourf: Je n'aime pas la read -rnotation. IMO, POSIX s'est trompé; L'option doit activer la signification spéciale pour les barres obliques inverses de fin, et non la désactiver - afin que les scripts existants (antérieurs à POSIX) ne se cassent pas car le a -rété omis. J'observe, cependant, qu'il faisait partie de l'IEEE 1003.2 1992, qui était la première version de la norme shell et utilitaires POSIX, mais il a été marqué comme un ajout même à ce moment-là, ce qui fait chier des opportunités de longue date. Je n'ai jamais rencontré de problèmes car mon code n'utilise pas -r; Je dois avoir de la chance. Ignorez-moi à ce sujet.
Jonathan Leffler
1
@JonathanLeffler Je suis vraiment d'accord que cela -rdevrait être standard. Je suis d'accord qu'il est peu probable que ce soit dans les cas où ne pas l'utiliser entraîne des problèmes. Cependant, un code cassé est un code cassé. Mon montage a d'abord été déclenché par cette pauvre $linevariable qui manquait beaucoup ses citations. J'ai corrigé le readtemps que j'y étais. Je n'ai pas corrigé le problème echocar c'est le type de modification qui est annulé. :(.
gniourf_gniourf
1

Le code ${1:-/dev/stdin}comprendra simplement le premier argument, alors, qu'en est-il.

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

Je ne trouve aucune de ces réponses acceptable. En particulier, la réponse acceptée ne gère que le premier paramètre de ligne de commande et ignore le reste. Le programme Perl qu'il essaie d'émuler gère tous les paramètres de ligne de commande. La réponse acceptée ne répond donc même pas à la question. D'autres réponses utilisent des extensions bash, ajoutent des commandes «cat» inutiles, ne fonctionnent que pour le cas simple d'écho d'entrée en sortie, ou sont simplement inutilement compliquées.

Cependant, je dois leur donner un certain crédit car ils m'ont donné quelques idées. Voici la réponse complète:

#!/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
la source
1

J'ai combiné toutes les réponses ci-dessus et créé une fonction shell qui conviendrait à mes besoins. C'est à partir d'un terminal cygwin de mes 2 machines Windows10 où j'avais un dossier partagé entre eux. Je dois être capable de gérer les éléments suivants:

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

Lorsqu'un nom de fichier spécifique est spécifié, je dois utiliser le même nom de fichier lors de la copie. Lorsque le flux de données d'entrée a été acheminé, j'ai besoin de générer un nom de fichier temporaire contenant les heures, les minutes et les secondes. Le dossier principal partagé a des sous-dossiers des jours de la semaine. C'est à des fins d'organisation.

Voici, le script ultime pour mes besoins:

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
}

S'il y a un moyen que vous pouvez voir pour optimiser davantage cela, je voudrais le savoir.

truthadjustr
la source
0

Ce qui suit fonctionne en standard sh(testé avec dashsur Debian) et est assez lisible, mais c'est une question de goût:

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

Détails: Si le premier paramètre n'est pas vide, alors catce fichier, sinon catentrée standard. Ensuite, la sortie de l' ifinstruction entière est traitée par le commands_and_transformations.

Pas dans la liste
la source
À mon humble avis la meilleure réponse parce qu'il pointe à la vraie solution: cat "${1:--}" | any_command. La lecture des variables shell et leur écho peuvent fonctionner pour les petits fichiers mais ne sont pas aussi bien mis à l'échelle.
Andreas Spindler
Le [ -n "$1" ]peut être simplifié en [ "$1" ].
agc
0

Celui-ci est facile à utiliser sur le terminal:

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

Que diriez-vous

for line in `cat`; do
    something($line);
done
Charles Cooper
la source
La sortie de catsera placée dans la ligne de commande. La ligne de commande a une taille maximale. De plus, cela ne lira pas ligne par ligne, mais mot par mot.
Notinlist