Looping melalui isi file di Bash

1390

Bagaimana cara saya mengulangi setiap baris file teks dengan Bash ?

Dengan skrip ini:

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

Saya mendapatkan output ini di layar:

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

(Nanti saya ingin melakukan sesuatu yang lebih rumit $pdari sekedar output ke layar.)


Variabel lingkungan SHELL adalah (dari env):

SHELL=/bin/bash

/bin/bash --version keluaran:

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

cat /proc/version keluaran:

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

File peptides.txt berisi:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Peter Mortensen
sumber
19
Oh, saya melihat banyak hal telah terjadi di sini: semua komentar telah dihapus dan pertanyaan dibuka kembali. Hanya untuk referensi, jawaban yang diterima di Baca file baris demi baris yang menetapkan nilai ke variabel mengatasi masalah dengan cara kanonik dan harus lebih disukai daripada yang diterima di sini.
fedorqui 'SO berhenti merugikan'

Jawaban:

2098

Salah satu cara untuk melakukannya adalah:

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

Seperti yang ditunjukkan dalam komentar, ini memiliki efek samping memangkas spasi putih terkemuka, menafsirkan urutan backslash, dan melewatkan baris terakhir jika tidak ada linefeed terminasi. Jika ini masalah, Anda dapat melakukan:

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

Khususnya, jika badan loop dapat membaca dari input standar , Anda dapat membuka file menggunakan deskriptor file yang berbeda:

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

Di sini, 10 hanyalah angka acak (berbeda dari 0, 1, 2).

Bruno De Fraine
sumber
7
Bagaimana saya menafsirkan baris terakhir? File peptides.txt dialihkan ke input standar dan entah bagaimana ke seluruh blok while?
Peter Mortensen
11
"Slurp peptides.txt ke loop sementara ini, jadi perintah 'baca' memiliki sesuatu untuk dikonsumsi." Metode "cat" saya mirip, mengirimkan output dari perintah ke blok sementara untuk konsumsi dengan 'membaca', juga, hanya meluncurkan program lain untuk menyelesaikan pekerjaan.
Warren Young
8
Metode ini tampaknya melewati baris terakhir file.
xastor
5
Kutip ganda garisnya !! gema "$ p" dan file .. percayalah itu akan menggigit Anda jika Anda tidak !!! AKU TAHU! lol
Mike Q
5
Kedua versi gagal membaca baris terakhir jika tidak diakhiri dengan baris baru. Selalu gunakanwhile read p || [[ -n $p ]]; do ...
dawg
448
cat peptides.txt | while read line 
do
   # do something with $line here
done

dan varian satu-baris:

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

Opsi-opsi ini akan melewati baris terakhir file jika tidak ada umpan garis tertinggal.

Anda dapat menghindari ini dengan yang berikut:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done
Warren Young
sumber
68
Secara umum, jika Anda menggunakan "cat" dengan hanya satu argumen, Anda melakukan sesuatu yang salah (atau kurang optimal).
JesperE
27
Ya, itu tidak seefisien Bruno, karena meluncurkan program lain, tidak perlu. Jika efisiensi penting, lakukan dengan cara Bruno. Saya ingat cara saya karena Anda dapat menggunakannya dengan perintah lain, di mana sintaks "redirect masuk dari" tidak berfungsi.
Warren Young
74
Ada masalah lain yang lebih serius dengan ini: karena loop sementara adalah bagian dari sebuah pipeline, ia berjalan dalam sebuah subkulit, dan karenanya setiap variabel yang diatur di dalam loop tersebut hilang ketika keluar (lihat bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Ini bisa sangat menjengkelkan (tergantung pada apa yang Anda coba lakukan dalam loop).
Gordon Davisson
25
Saya menggunakan "file cat |" sebagai awal dari banyak perintah saya murni karena saya sering prototipe dengan "file kepala |"
mat kelcey
62
Ini mungkin tidak seefisien itu, tetapi jauh lebih mudah dibaca daripada jawaban lain.
Savage Reader
144

Opsi 1a: Loop sementara: Satu baris sekaligus: Pengalihan input

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

Opsi 1b: Sementara loop: Baris tunggal pada satu waktu:
Buka file, baca dari deskriptor file (dalam hal ini file deskriptor # 4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done
Stan Graves
sumber
Untuk opsi 1b: apakah deskriptor file perlu ditutup lagi? Misalnya loop bisa menjadi loop dalam.
Peter Mortensen
3
Deskriptor file akan dibersihkan dengan proses keluar. Penutupan eksplisit dapat dilakukan untuk menggunakan kembali nomor fd. Untuk menutup fd, gunakan exec lain dengan sintaks & -, seperti ini: exec 4 <& -
Stan Graves
1
Terima kasih untuk Opsi 2. Saya mengalami masalah besar dengan Opsi 1 karena saya perlu membaca dari stdin dalam loop; dalam kasus seperti itu Opsi 1 tidak akan berfungsi.
masgo
4
Anda harus menunjukkan dengan lebih jelas bahwa Opsi 2 sangat tidak disarankan . @masgo Opsi 1b harus berfungsi dalam kasus itu, dan dapat dikombinasikan dengan sintaks redirection input dari Opsi 1a dengan mengganti done < $filenamedengan done 4<$filename(yang berguna jika Anda ingin membaca nama file dari parameter perintah, dalam hal ini Anda bisa mengganti $filenamedengan $1).
Egor Hans
Saya perlu mengulang isi file seperti tail -n +2 myfile.txt | grep 'somepattern' | cut -f3, ketika menjalankan perintah ssh di dalam loop (mengkonsumsi stdin); opsi 2 di sini tampaknya menjadi satu-satunya cara?
user5359531
85

Ini tidak lebih baik daripada jawaban lain, tetapi merupakan satu lagi cara untuk menyelesaikan pekerjaan dalam file tanpa spasi (lihat komentar). Saya menemukan bahwa saya sering perlu satu baris untuk menggali daftar dalam file teks tanpa langkah tambahan menggunakan file skrip yang terpisah.

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

Format ini memungkinkan saya untuk meletakkan semuanya dalam satu baris perintah. Ubah bagian "echo $ word" menjadi apa pun yang Anda inginkan dan Anda dapat mengeluarkan beberapa perintah yang dipisahkan oleh titik koma. Contoh berikut menggunakan konten file sebagai argumen ke dua skrip lain yang mungkin Anda tulis.

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

Atau jika Anda bermaksud untuk menggunakan ini seperti editor aliran (pelajari sed) Anda dapat membuang output ke file lain sebagai berikut.

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

Saya telah menggunakan ini seperti yang ditulis di atas karena saya telah menggunakan file teks di mana saya membuatnya dengan satu kata per baris. (Lihat komentar) Jika Anda memiliki spasi yang tidak ingin Anda pisahkan kata-kata / kalimat Anda, itu akan menjadi sedikit lebih buruk, tetapi perintah yang sama masih berfungsi sebagai berikut:

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

Ini hanya memberitahu shell untuk membagi pada baris baru saja, bukan spasi, lalu mengembalikan lingkungan kembali ke apa yang sebelumnya. Pada titik ini, Anda mungkin ingin mempertimbangkan untuk memasukkan semuanya ke dalam skrip shell daripada meremasnya menjadi satu baris.

Semoga berhasil!

mungkin
sumber
6
Bash $ (<peptides.txt) mungkin lebih elegan, tetapi masih salah, apa yang dikatakan Joao benar, Anda melakukan logika substitusi perintah di mana ruang atau baris baru adalah hal yang sama. Jika sebuah baris memiliki spasi di dalamnya, loop mengeksekusi DUA KALI atau lebih untuk satu baris. Jadi kode Anda harus benar dibaca: untuk kata dalam $ (<peptides.txt); lakukan .... Jika Anda tahu pasti tidak ada spasi, maka satu baris sama dengan satu kata dan Anda baik-baik saja.
maxpolk
2
@ JoaoCosta, maxpolk: Poin bagus yang belum saya pertimbangkan. Saya telah mengedit posting asli untuk mencerminkan mereka. Terima kasih!
mayypile
2
Menggunakan formembuat token input / garis tunduk pada ekspansi shell, yang biasanya tidak diinginkan; coba ini: for l in $(echo '* b c'); do echo "[$l]"; done- seperti yang akan Anda lihat, *- meskipun awalnya literal yang dikutip - diperluas ke file dalam direktori saat ini.
mklement0
2
@dblanchard: Contoh terakhir, menggunakan $ IFS, harus mengabaikan spasi. Sudahkah Anda mencoba versi itu?
mayypile
4
Cara bagaimana perintah ini menjadi jauh lebih kompleks ketika masalah-masalah penting diperbaiki, menyajikan dengan sangat baik mengapa menggunakan foruntuk mengulangi baris file adalah ide yang buruk. Plus, aspek ekspansi disebutkan oleh @ mklement0 (meskipun itu mungkin dapat dielakkan dengan membawa tanda kutip yang lolos, yang lagi-lagi membuat hal-hal menjadi lebih kompleks dan kurang dapat dibaca).
Egor Hans
69

Beberapa hal lagi yang tidak dicakup oleh jawaban lain:

Membaca dari file yang dibatasi

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

Membaca dari output perintah lain, menggunakan proses substitusi

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

Pendekatan ini lebih baik daripada command ... | while read -r line; do ...karena loop sementara di sini berjalan di shell saat ini daripada subkulit seperti dalam kasus yang terakhir. Lihat posting terkait Variabel yang diubah dalam loop sementara tidak diingat .

Membaca dari input terbatas nol, misalnya 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)

Terkait baca: BashFAQ / 020 - Bagaimana saya bisa menemukan dan dengan aman menangani nama file yang mengandung baris baru, spasi atau keduanya?

Membaca dari lebih dari satu file sekaligus

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

Berdasarkan jawaban @ chepner di sini :

-uadalah ekstensi bash. Untuk kompatibilitas POSIX, setiap panggilan akan terlihat seperti read -r X <&3.

Membaca seluruh file menjadi sebuah array (versi Bash sebelumnya ke 4)

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

Jika file berakhir dengan baris yang tidak lengkap (baris baru hilang di bagian akhir), maka:

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

Membaca seluruh file menjadi sebuah array (Bash versi 4x dan yang lebih baru)

readarray -t my_array < my_file

atau

mapfile -t my_array < my_file

Lalu

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

Posting terkait:

codeforester
sumber
perhatikan bahwa alih-alih command < input_filename.txtAnda selalu dapat melakukan input_generating_command | commandataucommand < <(input_generating_command)
masterxilo
1
Terima kasih telah membaca file ke dalam array. Persis apa yang saya butuhkan, karena saya perlu setiap baris untuk menguraikan dua kali, tambahkan ke variabel baru, lakukan beberapa validasi dll.
frank_108
45

Gunakan loop sementara, seperti ini:

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

Catatan:

  1. Jika Anda tidak mengatur dengan IFSbenar, Anda akan kehilangan lekukan.

  2. Anda hampir selalu harus menggunakan opsi -r dengan membaca.

  3. Jangan membaca baris dengan for

Jahid
sumber
2
Mengapa ada -rpilihan?
David C. Rankin
2
@ DavidC.Rankin Opsi -r mencegah interpretasi garis miring terbalik. Note #2adalah tautan yang dijelaskan secara terperinci ...
Jahid
Gabungkan ini dengan opsi "baca -u" di jawaban lain dan kemudian sempurna.
Florin Andrei
@FlorinAndrei: Contoh di atas tidak memerlukan -uopsi, apakah Anda berbicara tentang contoh lain dengan -u?
Jahid
Melihat melalui tautan Anda, dan terkejut tidak ada jawaban yang hanya menautkan tautan Anda di Catatan 2. Halaman itu menyediakan semua yang perlu Anda ketahui tentang subjek itu. Atau apakah jawaban tautan saja putus asa atau semacamnya?
Egor Hans
14

Misalkan Anda memiliki file ini:

$ 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

Ada empat elemen yang akan mengubah arti dari output file yang dibaca oleh banyak solusi Bash:

  1. Baris kosong 4;
  2. Ruang depan atau belakang pada dua garis;
  3. Mempertahankan makna garis individual (yaitu, setiap baris adalah catatan);
  4. Jalur 6 tidak diakhiri dengan CR.

Jika Anda ingin file teks baris demi baris termasuk baris kosong dan mengakhiri baris tanpa CR, Anda harus menggunakan loop sementara dan Anda harus memiliki tes alternatif untuk baris terakhir.

Berikut adalah metode yang dapat mengubah file (dibandingkan dengan apa yang catkembali):

1) Kehilangan baris terakhir dan spasi terdepan dan tertinggal:

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

(Jika Anda melakukannya while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt, Anda mempertahankan spasi di depan dan di belakang tetapi masih kehilangan baris terakhir jika tidak diakhiri dengan CR)

2) Menggunakan proses substitusi dengan catakan membaca seluruh file dalam satu tegukan dan kehilangan arti dari setiap baris:

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

(Jika Anda menghapus "dari $(cat /tmp/test.txt)Anda membaca file kata demi kata daripada satu tegukan. Juga mungkin bukan apa yang dimaksudkan ...)


Cara paling kuat dan paling sederhana untuk membaca file baris demi baris dan mempertahankan semua spasi adalah:

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

Jika Anda ingin menghapus ruang utama dan perdagangan, hapus IFS=bagian tersebut:

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

(File teks tanpa terminasi \n, sementara cukup umum, dianggap rusak di bawah POSIX. Jika Anda dapat mengandalkan trailing, \nAnda tidak perlu || [[ -n $line ]]dalam whileloop.)

Lebih banyak di BASH FAQ

dawg
sumber
13

Jika Anda tidak ingin bacaan Anda rusak oleh karakter baris baru, gunakan -

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

Kemudian jalankan skrip dengan nama file sebagai parameter.

Anjul Sharma
sumber
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
sumber
7
Jawaban ini membutuhkan peringatan yang disebutkan dalam jawaban mayypile , dan itu bisa gagal jika ada baris yang mengandung karakter meta shell (karena tanda kutip "$ x").
Toby Speight
7
Aku benar-benar kaget orang belum datang dengan yang biasa. Jangan baca baris untuk ...
Egor Hans
3

Berikut ini adalah contoh kehidupan nyata saya bagaimana untuk loop garis dari output program lain, periksa substring, drop tanda kutip ganda dari variabel, gunakan variabel itu di luar loop. Saya kira cukup banyak yang menanyakan pertanyaan ini cepat atau lambat.

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

Deklarasikan variabel di luar loop, atur nilai dan gunakan di luar loop yang harus dilakukan dengan sintaks <<< "$ (...)" . Aplikasi perlu dijalankan dalam konteks konsol saat ini. Kutipan di sekitar perintah menjaga baris arus keluaran baru.

Lingkaran yang cocok untuk substring kemudian membaca nama = pasangan nilai , membagi bagian sisi kanan dari karakter = terakhir , menjatuhkan kutipan pertama, menjatuhkan kutipan terakhir, kami memiliki nilai bersih untuk digunakan di tempat lain.

Siapa
sumber
3
Walaupun jawabannya benar, saya mengerti bagaimana jawabannya di sini. Metode esensial adalah sama seperti yang diusulkan oleh banyak jawaban lain. Plus, itu benar-benar tenggelam dalam contoh FPS Anda.
Egor Hans
0

Ini datang agak terlambat, tetapi dengan pemikiran bahwa itu dapat membantu seseorang, saya menambahkan jawabannya. Juga ini mungkin bukan cara terbaik. headperintah dapat digunakan dengan -nargumen untuk membaca n baris dari awal file dan juga tailperintah dapat digunakan untuk membaca dari bawah. Sekarang, untuk mengambil baris ke-n dari file, kita menuju n baris , menyalurkan data ke ekor hanya 1 baris dari data yang disalurkan.

   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
sumber
1
Jangan lakukan ini. Perulangan atas nomor baris dan mengambil setiap baris individu dengan cara sedatau head+ tailadalah sangat tidak efisien, dan tentu saja menimbulkan pertanyaan mengapa Anda tidak hanya menggunakan salah satu solusi lain di sini. Jika Anda perlu mengetahui nomor baris, tambahkan penghitung ke while read -rloop Anda , atau gunakan nl -bauntuk menambahkan awalan nomor baris ke setiap baris sebelum loop.
tripleee
-1

@ Peter: Ini bisa berhasil untuk Anda-

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

Ini akan mengembalikan output-

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Alan Jebakumar
sumber
11
Ini sangat buruk! Mengapa Anda tidak membaca baris dengan "untuk" .
fedorqui 'SO stop harming'
3
Jawaban ini mengalahkan semua prinsip yang ditetapkan oleh jawaban yang baik di atas!
codeforester
3
Silakan hapus jawaban ini.
dawg
3
Sekarang teman-teman, jangan berlebihan. Jawabannya buruk, tetapi tampaknya berhasil, setidaknya untuk kasus penggunaan sederhana. Selama itu diberikan, menjadi jawaban yang buruk tidak menghilangkan hak untuk ada.
Egor Hans
3
@ EgorHans, saya sangat tidak setuju: Inti dari jawaban adalah untuk mengajar orang bagaimana menulis perangkat lunak. Mengajar orang untuk melakukan sesuatu dengan cara yang Anda tahu berbahaya bagi mereka dan orang-orang yang menggunakan perangkat lunak mereka (memperkenalkan bug / perilaku tak terduga / dll) dengan sengaja merugikan orang lain. Sebuah jawaban yang diketahui berbahaya tidak memiliki "hak untuk hidup" dalam sumber daya pengajaran yang dikuratori dengan baik (dan mengkuratorinya adalah apa yang seharusnya kita, orang-orang yang pilih dan pilih, lakukan di sini).
Charles Duffy