web-dev-qa-db-fra.com

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

En Perl, le code suivant sera lu dans le fichier spécifié dans les arguments de la ligne de commande ou dans stdin:

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

C'est très pratique. Je veux juste savoir quel est le moyen le plus simple de lire un fichier ou stdin en bash.

195
Dagang

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

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

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

324
Fritz G. Mehner

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

#!/bin/bash
less <&0

Stdin est le descripteur de fichier zéro. Ce qui précède envoie les entrées vers votre script bash dans le stdin de less.

En savoir plus sur la redirection de descripteur de fichier .

89
Ryan Ballantyne

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 @ mklement0 comment ).


Pour analyser chaque ligne 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 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 traite pas un caractère barre oblique inversée de manière particulière. Considérez chaque barre oblique inverse comme faisant partie de la ligne de saisie.

- Sans définir IFS, par défaut les séquences de Space et Tab au début et à la fin des lignes sont ignorés (ajustés).

- Utilisez printf au lieu de echo pour éviter d’imprimer des lignes vides lorsque la ligne est constituée d’un seul -e, -n ou -E. Cependant, il existe une solution de contournement en utilisant env POSIXLY_CORRECT=1 echo "$line" qui exécute votre external GNU echo qui le prend en charge. Voir: Comment puis-je faire écho à "-e"?

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

63
kenorb

Je pense que c'est le moyen le plus simple:

$ 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
13
Amir Mehler

La solution echo ajoute de nouvelles lignes chaque fois que IFS interrompt le flux d'entrée. La réponse de @ fgm peut être modifiée un peu:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
13
David Souther

La boucle Perl de la question lit dans all les arguments du nom de fichier sur la ligne de commande ou dans 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 dérisoire avec précision comme UUOC (Utilisation inutile de cat), il est des moments où cat est le meilleur outil de travail, et on peut soutenir que c’est l’un d’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. Ainsi, des tâches telles que les affectations de variables dans la boucle while ne sont pas accessibles en dehors du pipeline. La solution bash est la suivante: Process Substitution :

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

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

6
Jonathan Leffler

Le comportement de Perl, avec le code indiqué dans l'OP, ne peut prendre aucun ou plusieurs arguments, et si un argument est un seul trait d'union -, il est compris comme étant 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 possibilité pure Bash. L'astuce consiste à utiliser exec de maniè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 dans $1.

Si aucun argument n'est fourni, nous avons artificiellement défini - en tant que premier paramètre de position. Nous bouclons ensuite sur les paramètres. Si un paramètre n'est pas -, nous redirigeons l'entrée standard à partir du nom de fichier avec exec. Si cette redirection réussit, nous bouclons avec une boucle while. J'utilise la variable standard REPLY et, dans ce cas, vous n'avez pas besoin de réinitialiser IFS. Si vous voulez un autre nom, vous devez réinitialiser IFS comme suit (à 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
4
gniourf_gniourf

Plus précisément...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
2
Sorpigal

S'il vous plaît essayez le code suivant:

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

Le code ${1:-/dev/stdin} comprendra simplement le premier argument, alors, que diriez-vous de cela.

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

J'ai combiné toutes les réponses ci-dessus et créé une fonction Shell qui conviendrait à mes besoins. Cela provient d'un terminal cygwin de mes 2 machines Windows10 où j'avais un dossier partagé entre elles. Je dois être capable de gérer ce qui suit:

  • 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é, alors je dois générer un nom de fichier temporaire ayant l'heure, les minutes et les secondes. Le dossier principal partagé contient les sous-dossiers des jours de la semaine. Ceci 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
}

Je voudrais savoir s’il est possible de l’optimiser davantage.

0
typelogic

Ce qui suit fonctionne avec la variable standard sh (testée avec dash sur 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 cat ce fichier, sinon cat entrée standard. Ensuite, la sortie de l'instruction if entière est traitée par le commands_and_transformations.

0
Notinlist

Je ne trouve aucune de ces réponses acceptables. 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. Donc, la réponse acceptée ne répond même pas à la question. D'autres réponses utilisent des extensions bash, ajoutent des commandes 'cat' inutiles, ne fonctionnent que dans le cas simple d'écho d'une entrée à l'autre ou sont simplement inutilement compliquées.

Cependant, je dois leur donner un peu de crédit car ils m'ont donné des 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
0
Gungwald