Scorrere il contenuto di un file in Bash


1390

Come posso scorrere tutte le righe di un file di testo con Bash ?

Con questo script:

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

Ottengo questo output sullo schermo:

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

(Più tardi voglio fare qualcosa di più complicato $prispetto all'output sullo schermo.)


La variabile d'ambiente SHELL è (da env):

SHELL=/bin/bash

/bin/bash --version produzione:

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

cat /proc/version produzione:

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

Il file peptides.txt contiene:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL

19
Oh, vedo che sono successe molte cose qui: tutti i commenti sono stati cancellati e la domanda è stata riaperta. Solo per riferimento, la risposta accettata in Leggi un file riga per riga assegnando il valore a una variabile risolve il problema in modo canonico e dovrebbe essere preferita a quella accettata qui.
fedorqui "SO smettere di danneggiare"

Risposte:


2098

Un modo per farlo è:

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

Come sottolineato nei commenti, ciò ha gli effetti collaterali del taglio degli spazi bianchi iniziali, dell'interpretazione delle sequenze di barre rovesciate e del salto dell'ultima riga se manca un avanzamento di riga finale. Se queste sono preoccupazioni, puoi fare:

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

Eccezionalmente, se il corpo del loop può leggere dall'input standard , è possibile aprire il file utilizzando un descrittore di file diverso:

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

Qui, 10 è solo un numero arbitrario (diverso da 0, 1, 2).


7
Come devo interpretare l'ultima riga? Il file peptides.txt viene reindirizzato all'input standard e in qualche modo all'intero blocco while?
Peter Mortensen,

11
"Slurp peptides.txt in questo ciclo while, quindi il comando 'read' ha qualcosa da consumare." Il mio metodo "cat" è simile, inviando l'output di un comando nel blocco while per il consumo anche da 'read', solo che avvia un altro programma per completare il lavoro.
Warren Young,

8
Questo metodo sembra saltare l'ultima riga di un file.
xastor,

5
Doppia citazione delle righe !! echo "$ p" e il file .. fidati di me ti morderà se non lo fai !!! LO SO! lol
Mike Q,

5
Entrambe le versioni non riescono a leggere un'ultima riga se non termina con una nuova riga. Usa semprewhile read p || [[ -n $p ]]; do ...
dawg

449
cat peptides.txt | while read line 
do
   # do something with $line here
done

e la variante one-liner:

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

Queste opzioni salteranno l'ultima riga del file se non vi sono feed di riga finali.

Puoi evitarlo come segue:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done

68
In generale, se stai usando "cat" con un solo argomento, stai facendo qualcosa di sbagliato (o subottimale).
JesperE

27
Sì, non è così efficiente come quello di Bruno, perché lancia un altro programma, inutilmente. Se l'efficienza conta, fallo alla maniera di Bruno. Ricordo la mia strada perché puoi usarla con altri comandi, dove la sintassi "reindirizzamento da" non funziona.
Warren Young,

74
C'è un altro problema più grave con questo: poiché il ciclo while fa parte di una pipeline, viene eseguito in una subshell e quindi tutte le variabili impostate all'interno del ciclo vengono perse quando esce (vedere bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Questo può essere molto fastidioso (a seconda di cosa stai cercando di fare nel loop).
Gordon Davisson,

25
Uso "cat file |" come inizio di molti dei miei comandi solo perché spesso prototipo con "head file |"
mat kelcey,

62
Questo potrebbe non essere così efficiente, ma è molto più leggibile rispetto ad altre risposte.
Savage Reader,

144

Opzione 1a: ciclo While: linea singola alla volta: reindirizzamento input

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

Opzione 1b: ciclo While: riga singola alla volta:
aprire il file, leggere da un descrittore di file (in questo caso il descrittore di file n. 4).

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

Per l'opzione 1b: il descrittore di file deve essere nuovamente chiuso? Ad esempio il loop potrebbe essere un loop interno.
Peter Mortensen,

3
Il descrittore di file verrà ripulito con le uscite del processo. È possibile eseguire una chiusura esplicita per riutilizzare il numero fd. Per chiudere un fd, usa un altro exec con la sintassi & -, in questo modo: exec 4 <& -
Stan Graves,

1
Grazie per l'Opzione 2. Ho riscontrato grossi problemi con l'Opzione 1 perché dovevo leggere da stdin all'interno del loop; in tal caso, l'Opzione 1 non funzionerà.
Masgo,

4
Dovresti sottolineare più chiaramente che l'opzione 2 è fortemente scoraggiata . L'opzione 1mas di @masgo dovrebbe funzionare in quel caso e può essere combinata con la sintassi di reindirizzamento di input dall'opzione 1a sostituendo done < $filenamecon done 4<$filename(che è utile se si desidera leggere il nome del file da un parametro di comando, nel qual caso è sufficiente sostituire $filenamecon $1).
Egor Hans,

Ho bisogno di tail -n +2 myfile.txt | grep 'somepattern' | cut -f3scorrere il contenuto del file come , mentre eseguo i comandi ssh all'interno del ciclo (consuma stdin); l'opzione 2 qui sembra essere l'unico modo?
user5359531

85

Questo non è meglio di altre risposte, ma è un altro modo per eseguire il lavoro in un file senza spazi (vedi commenti). Trovo che ho spesso bisogno di una riga per cercare gli elenchi nei file di testo senza il passaggio aggiuntivo di utilizzare file di script separati.

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

Questo formato mi consente di mettere tutto in una riga di comando. Cambia la parte "echo $ word" come preferisci e puoi emettere più comandi separati da punti e virgola. L'esempio seguente usa i contenuti del file come argomenti in altri due script che potresti aver scritto.

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

Oppure, se hai intenzione di usarlo come un editor di stream (impara sed) puoi scaricare l'output su un altro file come segue.

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

Ho usato questi come scritto sopra perché ho usato file di testo in cui li ho creati con una parola per riga. (Vedi commenti) Se hai spazi che non vuoi dividere le tue parole / linee, diventa un po 'più brutto, ma lo stesso comando funziona ancora come segue:

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

Questo dice alla shell di dividere solo su newline, non su spazi, quindi riporta l'ambiente a quello che era in precedenza. A questo punto, potresti prendere in considerazione l'idea di mettere tutto in uno script di shell piuttosto che comprimerlo in un'unica riga.

Buona fortuna!


6
Il bash $ (<peptides.txt) è forse più elegante, ma è ancora sbagliato, ciò che Joao ha detto corretto, stai eseguendo una logica di sostituzione dei comandi in cui spazio o newline sono la stessa cosa. Se una linea ha uno spazio al suo interno, il ciclo esegue DUE VOLTE o più per quella linea. Quindi il tuo codice dovrebbe leggere correttamente: for word in $ (<peptides.txt); fare .... Se sai per certo che non ci sono spazi, allora una linea equivale a una parola e stai bene.
maxpolk,

2
@ JoaoCosta, maxpolk: buoni punti che non avevo considerato. Ho modificato il post originale per rispecchiarli. Grazie!
poderoso

2
L'uso di forrende i token / le linee di input soggetti a espansioni della shell, che di solito è indesiderabile; prova questo: for l in $(echo '* b c'); do echo "[$l]"; done- come vedrai, il *- anche se originariamente un letterale tra virgolette - si espande ai file nella directory corrente.
mklement0

2
@dblanchard: l'ultimo esempio, usando $ IFS, dovrebbe ignorare gli spazi. Hai provato quella versione?
poderoso

4
Il modo in cui questo comando diventa molto più complesso quando vengono risolti problemi cruciali, mostra molto bene perché usare forper iterare le righe dei file sia una cattiva idea. Inoltre, l'aspetto di espansione menzionato da @ mklement0 (anche se probabilmente ciò può essere aggirato introducendo virgolette sfuggite, il che rende di nuovo le cose più complesse e meno leggibili).
Egor Hans,

69

Alcune altre cose non coperte da altre risposte:

Lettura da un file delimitato

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

Leggere dall'output di un altro comando, usando la sostituzione del processo

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

Questo approccio è migliore rispetto al command ... | while read -r line; do ...fatto che il ciclo while viene eseguito nella shell corrente anziché in una subshell come nel caso di quest'ultima. Vedi il relativo post Una variabile modificata all'interno di un ciclo while non viene ricordata .

Lettura da un input delimitato da null, ad esempio 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)

Leggi correlate: BashFAQ / 020 - Come posso trovare e gestire in modo sicuro i nomi dei file contenenti newline, spazi o entrambi?

Lettura da più di un file alla volta

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

Basato sulla risposta di @ chepner qui :

-uè un'estensione bash. Per la compatibilità POSIX, ogni chiamata sarebbe simile read -r X <&3.

Lettura di un intero file in un array (versioni di Bash precedenti alla 4)

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

Se il file termina con una riga incompleta (newline mancante alla fine), quindi:

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

Lettura di un intero file in un array (versioni di Bash 4x e successive)

readarray -t my_array < my_file

o

mapfile -t my_array < my_file

E poi

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

Articoli correlati:


nota che al posto command < input_filename.txttuo puoi sempre fare input_generating_command | commandocommand < <(input_generating_command)
masterxilo

1
Grazie per aver letto il file nell'array. Esattamente quello di cui ho bisogno, perché ho bisogno che ogni riga
venga

45

Usa un ciclo while, in questo modo:

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

Appunti:

  1. Se non si imposta IFScorrettamente, si perderà il rientro.

  2. Dovresti quasi sempre usare l'opzione -r con read.

  3. Non leggere le righe con for


2
Perché l' -ropzione?
David C. Rankin,

2
@ DavidC.Rankin L'opzione -r impedisce l'interpretazione della barra rovesciata. Note #2è un collegamento in cui è descritto in dettaglio ...
Jahid,

Combina questo con l'opzione "read -u" in un'altra risposta ed è perfetto.
Florin Andrei,

@FlorinAndrei: l'esempio sopra non ha bisogno -udell'opzione, stai parlando di un altro esempio con -u?
Jahid,

Ho esaminato i tuoi collegamenti e sono rimasto sorpreso dal fatto che non vi sia alcuna risposta che colleghi semplicemente i tuoi collegamenti nella Nota 2. Quella pagina fornisce tutto ciò che devi sapere sull'argomento. O le risposte solo link sono scoraggiate o qualcosa del genere?
Egor Hans,

14

Supponiamo di avere questo file:

$ 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

Esistono quattro elementi che alterano il significato dell'output del file letto da molte soluzioni Bash:

  1. La riga vuota 4;
  2. Spazi iniziali o finali su due linee;
  3. Mantenere il significato delle singole linee (cioè ogni linea è un record);
  4. La riga 6 non è terminata con un CR.

Se si desidera il file di testo riga per riga, comprese le righe vuote e le righe di fine senza CR, è necessario utilizzare un ciclo while e disporre di un test alternativo per la riga finale.

Ecco i metodi che possono modificare il file (rispetto a ciò che catrestituisce):

1) Perdere l'ultima riga e gli spazi iniziali e finali:

$ 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'

(Se lo fai while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txtinvece, conservi gli spazi iniziali e finali ma perdi comunque l'ultima riga se non termina con CR)

2) Usando la sostituzione del processo con catwill si legge l'intero file in un sorso e si perde il significato delle singole righe:

$ 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'

(Se rimuovi "da $(cat /tmp/test.txt)te leggi il file parola per parola piuttosto che un sorso. Probabilmente non è quello che intendi ...)


Il modo più robusto e semplice per leggere un file riga per riga e preservare tutta la spaziatura è:

$ 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'

Se desideri rimuovere gli spazi iniziali e commerciali, rimuovi 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 file di testo senza terminazione \n, sebbene abbastanza comune, è considerato rotto in POSIX. Se puoi contare sul trascinamento \nnon è necessario || [[ -n $line ]]nelwhile loop.)

Maggiori informazioni sulle FAQ di BASH


13

Se non desideri che la lettura venga interrotta dal carattere di nuova riga, usa -

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

Quindi eseguire lo script con il nome file come parametro.


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

7
Questa risposta richiede le avvertenze menzionate nella risposta di potenti file e può fallire gravemente se una riga contiene metacaratteri di shell (a causa della "$ x" non quotata).
Toby Speight,

7
In realtà sono sorpreso che la gente non abbia ancora escogitato il solito non leggere le righe con per ...
Egor Hans,

3

Ecco il mio esempio di vita reale su come eseguire il loop delle righe di un altro output del programma, controllare le sottostringhe, eliminare le virgolette doppie dalla variabile, utilizzare quella variabile al di fuori del loop. Immagino che molti prima o poi facciano queste domande.

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

Dichiarare la variabile al di fuori del ciclo, impostare il valore e usarlo al di fuori del ciclo richiede la sintassi <<< "$ (...)" . L'applicazione deve essere eseguita in un contesto della console corrente. Le virgolette intorno al comando mantengono le nuove linee del flusso di output.

La corrispondenza del ciclo per le sottostringhe quindi legge la coppia nome = valore , divide la parte destra dell'ultimo = carattere, elimina la prima virgoletta, elimina l'ultima citazione, abbiamo un valore pulito da usare altrove.


3
Mentre la risposta è corretta, capisco come sia finita qui. Il metodo essenziale è lo stesso proposto da molte altre risposte. Inoltre, annega completamente nell'esempio di FPS.
Egor Hans,

0

Questo arriverà piuttosto tardi, ma con l'idea che possa aiutare qualcuno, sto aggiungendo la risposta. Anche questo potrebbe non essere il modo migliore. headIl comando può essere usato con -nargomento per leggere n righe dall'inizio del file e allo stesso modo il tailcomando può essere letto dal basso. Ora, per recuperare l' ennesima riga dal file, andiamo a capo di n righe , reindirizziamo i dati in modo che seguano solo 1 riga dai dati inviati.

   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

1
Non farlo Il ciclo sopra i numeri di riga e il recupero di ogni singola riga tramite sedo head+ tailè incredibilmente inefficiente, e ovviamente pone la domanda sul perché non usi semplicemente una delle altre soluzioni qui. Se è necessario conoscere il numero di riga, aggiungere un contatore al while read -rloop o utilizzare nl -baper aggiungere un prefisso del numero di riga a ciascuna riga prima del loop.
triplo il

-1

@Peter: Questo potrebbe funzionare per te-

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

Ciò restituirebbe l'output:

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL


3
Questa risposta sta sconfiggendo tutti i principi stabiliti dalle buone risposte sopra!
codeforester

3
Elimina questa risposta.
dawg,

3
Ragazzi, non esagerate. La risposta è negativa, ma sembra funzionare, almeno per casi d'uso semplici. Finché viene fornita, essere una cattiva risposta non toglie il diritto alla risposta di esistere.
Egor Hans,

3
@EgorHans, non sono d'accordo: il punto di risposta è insegnare alle persone come scrivere software. Insegnare alle persone a fare le cose in un modo che sai è dannoso per loro e le persone che usano il loro software (introducendo bug / comportamenti imprevisti / ecc.) Danneggiano consapevolmente gli altri. Una risposta nota per essere dannosa non ha "diritto di esistere" in una risorsa didattica ben curata (e curarla è esattamente ciò che noi, persone che votiamo e segnaliamo, dovremmo fare qui).
Charles Duffy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.