Parcourir le contenu d'un fichier dans Bash

1391

Comment parcourir chaque ligne d'un fichier texte avec Bash ?

Avec ce script:

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

J'obtiens cette sortie sur l'écran:

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

(Plus tard, je veux faire quelque chose de plus compliqué $pque de simplement afficher sur l'écran.)


La variable d'environnement SHELL est (de env):

SHELL=/bin/bash

/bin/bash --version production:

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

cat /proc/version production:

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

Le fichier peptides.txt contient:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Peter Mortensen
la source
19
Oh, je vois que beaucoup de choses se sont passées ici: tous les commentaires ont été supprimés et la question a été rouverte. Juste pour référence, la réponse acceptée dans Lire un fichier ligne par ligne attribuant la valeur à une variable résout le problème de manière canonique et doit être préférée à celle acceptée ici.
fedorqui 'SO stop harming'

Réponses:

2099

Une façon de le faire est:

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

Comme indiqué dans les commentaires, cela a les effets secondaires de rogner les espaces blancs de tête, d'interpréter les séquences de barre oblique inverse et de sauter la dernière ligne s'il manque un saut de ligne de fin. Si ce sont des préoccupations, vous pouvez faire:

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

Exceptionnellement, si le corps de la boucle peut lire à partir de l'entrée standard , vous pouvez ouvrir le fichier à l'aide d'un descripteur de fichier différent:

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

Ici, 10 n'est qu'un nombre arbitraire (différent de 0, 1, 2).

Bruno De Fraine
la source
7
Comment dois-je interpréter la dernière ligne? Le fichier peptides.txt est redirigé vers l'entrée standard et en quelque sorte vers l'ensemble du bloc while?
Peter Mortensen
11
"Slurp peptides.txt dans cette boucle while, donc la commande 'read' a quelque chose à consommer." Ma méthode "cat" est similaire, en envoyant la sortie d'une commande dans le bloc while pour la consommation par "lecture", seulement elle lance un autre programme pour faire le travail.
Warren Young
8
Cette méthode semble ignorer la dernière ligne d'un fichier.
xastor
5
Citez les lignes deux fois !! echo "$ p" et le fichier .. croyez-moi, il vous mordra si vous ne le faites pas !!! JE CONNAIS! lol
Mike Q
5
Les deux versions ne parviennent pas à lire une ligne finale si elle n'est pas terminée par une nouvelle ligne. Toujours utiliserwhile read p || [[ -n $p ]]; do ...
dawg
449
cat peptides.txt | while read line 
do
   # do something with $line here
done

et la variante monoligne:

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

Ces options ignoreront la dernière ligne du fichier s'il n'y a pas de saut de ligne de fin.

Vous pouvez éviter cela en procédant comme suit:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done
Warren Young
la source
68
En général, si vous utilisez "cat" avec un seul argument, vous faites quelque chose de mal (ou sous-optimal).
JesperE
27
Oui, ce n'est pas aussi efficace que celui de Bruno, car il lance inutilement un autre programme. Si l'efficacité compte, faites-le à la manière de Bruno. Je me souviens de mon chemin car vous pouvez l'utiliser avec d'autres commandes, où la syntaxe "rediriger depuis" ne fonctionne pas.
Warren Young
74
Il y a un autre problème plus grave avec cela: parce que la boucle while fait partie d'un pipeline, elle s'exécute dans un sous-shell, et donc toutes les variables définies à l'intérieur de la boucle sont perdues à sa sortie (voir bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Cela peut être très ennuyeux (selon ce que vous essayez de faire dans la boucle).
Gordon Davisson
25
J'utilise "cat file |" comme le début de beaucoup de mes commandes uniquement parce que je fais souvent un prototype avec "head file |"
mat kelcey
62
Ce n'est peut-être pas si efficace, mais c'est beaucoup plus lisible que les autres réponses.
Savage Reader
145

Option 1a: Pendant la boucle: Une seule ligne à la fois: Redirection d'entrée

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

Option 1b: Pendant la boucle: Une seule ligne à la fois:
Ouvrez le fichier, lisez à partir d'un descripteur de fichier (dans ce cas, le descripteur de fichier # 4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done
Stan Graves
la source
Pour l'option 1b: le descripteur de fichier doit-il être refermé? Par exemple, la boucle pourrait être une boucle intérieure.
Peter Mortensen
3
Le descripteur de fichier sera nettoyé avec la fermeture du processus. Une fermeture explicite peut être effectuée pour réutiliser le nombre fd. Pour fermer un fd, utilisez un autre exec avec la syntaxe & -, comme ceci: exec 4 <& -
Stan Graves
1
Merci pour l'option 2. J'ai rencontré d'énormes problèmes avec l'option 1 car j'avais besoin de lire depuis stdin dans la boucle; dans ce cas, l'option 1 ne fonctionnera pas.
masgo
4
Vous devez souligner plus clairement que l'option 2 est fortement déconseillée . @masgo L'option 1b devrait fonctionner dans ce cas et peut être combinée avec la syntaxe de redirection d'entrée de l'option 1a en remplaçant done < $filenamepar done 4<$filename(ce qui est utile si vous souhaitez lire le nom de fichier à partir d'un paramètre de commande, auquel cas vous pouvez simplement le remplacer $filenamepar $1).
Egor Hans
J'ai besoin de faire une boucle sur le contenu du fichier comme tail -n +2 myfile.txt | grep 'somepattern' | cut -f3, lors de l'exécution des commandes ssh à l'intérieur de la boucle (consomme stdin); l'option 2 ici semble être la seule façon?
user5359531
85

Ce n'est pas mieux que les autres réponses, mais c'est une autre façon de faire le travail dans un fichier sans espaces (voir commentaires). Je trouve que j'ai souvent besoin de lignes simples pour fouiller les listes dans les fichiers texte sans l'étape supplémentaire d'utiliser des fichiers de script séparés.

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

Ce format me permet de tout mettre en une seule ligne de commande. Changez la portion "echo $ word" en ce que vous voulez et vous pouvez émettre plusieurs commandes séparées par des points-virgules. L'exemple suivant utilise le contenu du fichier comme arguments dans deux autres scripts que vous avez peut-être écrits.

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

Ou si vous avez l'intention de l'utiliser comme un éditeur de flux (learn sed), vous pouvez vider la sortie dans un autre fichier comme suit.

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

Je les ai utilisés comme écrit ci-dessus parce que j'ai utilisé des fichiers texte où je les ai créés avec un mot par ligne. (Voir les commentaires) Si vous avez des espaces que vous ne voulez pas séparer vos mots / lignes, cela devient un peu plus laid, mais la même commande fonctionne toujours comme suit:

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

Cela indique simplement au shell de se diviser uniquement sur les nouvelles lignes, pas sur les espaces, puis ramène l'environnement à ce qu'il était auparavant. À ce stade, vous voudrez peut-être envisager de tout mettre dans un script shell plutôt que de le regrouper sur une seule ligne.

Bonne chance!

mayypile
la source
6
Le bash $ (<peptides.txt) est peut-être plus élégant, mais il est toujours faux, ce que Joao a dit correct, vous effectuez une logique de substitution de commande où espace ou nouvelle ligne est la même chose. Si une ligne contient un espace, la boucle exécute DEUX FOIS ou plus pour cette seule ligne. Donc, votre code devrait lire correctement: pour le mot dans $ (<peptides.txt); faire .... Si vous savez pertinemment qu'il n'y a pas d'espaces, alors une ligne est égale à un mot et ça va.
maxpolk
2
@ JoaoCosta, maxpolk: Bons points que je n'avais pas pris en compte. J'ai modifié le message d'origine pour les refléter. Merci!
mightypile
2
L'utilisation forrend les jetons / lignes d'entrée sujets à des extensions de shell, ce qui n'est généralement pas souhaitable; essayez ceci: for l in $(echo '* b c'); do echo "[$l]"; done- comme vous le verrez, le *- même si à l'origine un littéral cité - se développe dans les fichiers du répertoire courant.
mklement0
2
@dblanchard: Le dernier exemple, utilisant $ IFS, devrait ignorer les espaces. Avez-vous essayé cette version?
mightypile
4
La façon dont cette commande devient beaucoup plus complexe à mesure que les problèmes cruciaux sont résolus, présente très bien pourquoi l'utilisation forpour itérer les lignes de fichiers est une mauvaise idée. De plus, l'aspect d'expansion mentionné par @ mklement0 (même si cela peut probablement être contourné en introduisant des guillemets échappés, ce qui rend les choses plus complexes et moins lisibles).
Egor Hans
69

Quelques autres choses non couvertes par d'autres réponses:

Lecture à partir d'un fichier délimité

# ':' 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

Lecture à partir de la sortie d'une autre commande, en utilisant la substitution de processus

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

Cette approche est meilleure que command ... | while read -r line; do ...parce que la boucle while s'exécute ici dans le shell actuel plutôt que dans un sous-shell comme dans le cas de ce dernier. Voir le post associé Une variable modifiée à l'intérieur d'une boucle while n'est pas mémorisée .

Lecture à partir d'une entrée délimitée par des valeurs nulles, par exemple 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)

Lecture connexe: BashFAQ / 020 - Comment puis-je trouver et gérer en toute sécurité les noms de fichiers contenant des sauts de ligne, des espaces ou les deux?

Lecture à partir de plusieurs fichiers à la fois

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

Basé sur la réponse de @ chepner ici :

-uest une extension bash. Pour la compatibilité POSIX, chaque appel ressemblerait à quelque chose commeread -r X <&3 .

Lecture d'un fichier entier dans un tableau (versions Bash antérieures à 4)

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

Si le fichier se termine par une ligne incomplète (saut de ligne manquant à la fin), alors:

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

Lecture d'un fichier entier dans un tableau (versions Bash 4x et ultérieures)

readarray -t my_array < my_file

ou

mapfile -t my_array < my_file

Et alors

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

Articles Similaires:

codeforester
la source
notez qu'au lieu de command < input_filename.txtvous pouvez toujours faire input_generating_command | commandoucommand < <(input_generating_command)
masterxilo
1
Merci d'avoir lu le fichier dans le tableau. Exactement ce dont j'ai besoin, car j'ai besoin que chaque ligne analyse deux fois, ajoute de nouvelles variables, fasse des validations, etc.
frank_108
45

Utilisez une boucle while, comme ceci:

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

Remarques:

  1. Si vous ne définissez pas IFScorrectement, vous perdrez l'indentation.

  2. Vous devriez presque toujours utiliser l'option -r avec read.

  3. Ne lisez pas les lignes avec for

Jahid
la source
2
Pourquoi l' -roption?
David C. Rankin
2
@ DavidC.Rankin L'option -r empêche l'interprétation de la barre oblique inverse. Note #2est un lien où il est décrit en détail ...
Jahid
Combinez cela avec l'option "read -u" dans une autre réponse et c'est parfait.
Florin Andrei
@FlorinAndrei: L'exemple ci-dessus n'a pas besoin de l' -uoption, parlez-vous d'un autre exemple avec -u?
Jahid
Regardé à travers vos liens, et a été surpris qu'il n'y ait pas de réponse qui relie simplement votre lien dans la note 2. Cette page fournit tout ce que vous devez savoir sur ce sujet. Ou les réponses en lien uniquement sont-elles découragées ou quelque chose?
Egor Hans
14

Supposons que vous ayez ce fichier:

$ 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

Il existe quatre éléments qui modifieront la signification de la sortie de fichier lue par de nombreuses solutions Bash:

  1. La ligne vierge 4;
  2. Espaces de début ou de fin sur deux lignes;
  3. Conserver la signification des lignes individuelles (c.-à-d., Chaque ligne est un enregistrement);
  4. La ligne 6 ne se termine pas par un CR.

Si vous souhaitez que le fichier texte ligne par ligne, y compris les lignes vides et les lignes de terminaison sans CR, vous devez utiliser une boucle while et vous devez avoir un test alternatif pour la ligne finale.

Voici les méthodes qui peuvent changer le fichier (par rapport à ce qui catretourne):

1) Perdez la dernière ligne et les espaces de début et de fin:

$ 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 vous le faites à la while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txtplace, vous conservez les espaces de début et de fin mais perdez toujours la dernière ligne si elle ne se termine pas par CR)

2) L'utilisation de la substitution de processus avec catwill lit le fichier entier en une seule gorgée et perd la signification des lignes individuelles:

$ 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 vous enlevez le "de $(cat /tmp/test.txt)vous lisez le fichier mot par mot plutôt qu'une gorgée. Aussi probablement pas ce qui est prévu ...)


La façon la plus robuste et la plus simple de lire un fichier ligne par ligne et de conserver tout l'espacement est:

$ 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 vous souhaitez supprimer les espaces de tête et d'échange, supprimez la IFS=pièce:

$ 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 fichier texte sans terminaison \n, si vous pouvez compter sur l'arrière tout assez commun, est considéré comme rompu sous Posix. \nVous n'avez pas besoin || [[ -n $line ]]dans lawhile boucle.)

Plus à la FAQ BASH

dawg
la source
13

Si vous ne voulez pas que votre lecture soit interrompue par un caractère de nouvelle ligne, utilisez -

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

Exécutez ensuite le script avec le nom de fichier comme paramètre.

Anjul Sharma
la source
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
Sinus
la source
7
Cette réponse a besoin des mises en garde mentionnées dans la réponse de mightypile , et elle peut échouer gravement si une ligne contient des métacaractères shell (en raison du "$ x" non cité ).
Toby Speight
7
Je suis en fait surpris que les gens n'aient pas encore trouvé l'habituel Ne lisez pas les lignes avec pour ...
Egor Hans
3

Voici mon exemple réel comment boucler des lignes d'une autre sortie de programme, vérifier les sous-chaînes, supprimer les guillemets doubles de la variable, utiliser cette variable en dehors de la boucle. Je suppose que beaucoup posent ces questions tôt ou tard.

##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

Déclarez la variable en dehors de la boucle, définissez la valeur et utilisez-la en dehors de la boucle nécessite de faire <<< "$ (...)" syntaxe . L'application doit être exécutée dans un contexte de console actuelle. Les citations autour de la commande gardent les nouvelles lignes du flux de sortie.

La correspondance de boucle pour les sous-chaînes lit ensuite la paire nom = valeur , divise la partie droite du dernier caractère = , supprime la première citation, supprime la dernière citation, nous avons une valeur propre à utiliser ailleurs.

Qui
la source
3
Bien que la réponse soit correcte, je comprends comment cela s'est terminé ici. La méthode essentielle est la même que celle proposée par de nombreuses autres réponses. De plus, il se noie complètement dans votre exemple FPS.
Egor Hans
0

Cela arrive assez tard, mais avec l'idée que cela peut aider quelqu'un, j'ajoute la réponse. De plus, ce n'est peut-être pas la meilleure façon. headLa commande peut être utilisée avec un -nargument pour lire n lignes depuis le début du fichier et la tailcommande peut également être utilisée pour lire depuis le bas. Maintenant, pour récupérer la nième ligne du fichier, nous dirigeons n lignes , redirigeons les données vers une seule ligne à partir des données canalisées.

   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
la source
1
Ne fais pas ça. La boucle sur les numéros de ligne et la récupération de chaque ligne individuelle via sedou head+ tailest incroyablement inefficace, et pose bien sûr la question de savoir pourquoi vous n'utilisez pas simplement l'une des autres solutions ici. Si vous devez connaître le numéro de ligne, ajoutez un compteur à votre while read -rboucle ou utilisez nl -bapour ajouter un préfixe de numéro de ligne à chaque ligne avant la boucle.
tripleee
0

J'aime utiliser xargsau lieu de while. xargsest puissant et convivial en ligne de commande

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

Avec xargs, vous pouvez également ajouter de la verbosité -tet une validation avec-p

hamou92
la source
-1

@Peter: Cela pourrait fonctionner pour vous-

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

Cela retournerait la sortie-

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Alan Jebakumar
la source
11
C'est très mauvais! Pourquoi vous ne lisez pas les lignes avec "pour" .
fedorqui 'SO arrête de nuire'
3
Cette réponse va à l'encontre de tous les principes établis par les bonnes réponses ci-dessus!
codeforester
3
Veuillez supprimer cette réponse.
dawg
3
Maintenant, les gars, n'exagérez pas. La réponse est mauvaise, mais elle semble fonctionner, du moins pour les cas d'utilisation simples. Tant que cela est fourni, être une mauvaise réponse n'enlève pas le droit à la réponse d'exister.
Egor Hans
3
@EgorHans, je ne suis pas du tout d'accord: l'objectif des réponses est d'enseigner aux gens comment écrire des logiciels. Apprendre aux gens à faire les choses d'une manière que vous savez leur est nocif et les personnes qui utilisent leur logiciel (introduisant des bogues / des comportements inattendus / etc.) nuisent sciemment aux autres. Une réponse connue pour être nuisible n'a pas de "droit d'exister" dans une ressource pédagogique bien organisée (et la conserver est exactement ce que nous, les gens qui votons et signalons, sommes censés faire ici).
Charles Duffy