Mengapa menggunakan shell loop untuk memproses teks dianggap praktik buruk?

196

Apakah menggunakan loop sementara untuk memproses teks secara umum dianggap praktik buruk di shell POSIX?

Seperti yang ditunjukkan oleh Stéphane Chazelas , beberapa alasan untuk tidak menggunakan shell loop adalah konseptual , keandalan , keterbacaan , kinerja , dan keamanan .

Jawaban ini menjelaskan aspek keandalan dan keterbacaan :

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"

Untuk kinerja , whileloop dan baca sangat lambat saat membaca dari file atau pipa, karena shell baca built-in membaca satu karakter pada satu waktu.

Bagaimana dengan aspek konseptual dan keamanan ?

cuonglm
sumber
Terkait (sisi lain dari koin): Bagaimana cara yesmenulis ke file begitu cepat?
Wildcard
1
Built-in read shell tidak membaca satu karakter pada satu waktu, ia membaca satu baris pada satu waktu. wiki.bash-hackers.org/commands/builtin/read
A.Danischewski
@ A.Danischewski: Tergantung shell Anda. Di bash, ia membaca satu ukuran buffer sekaligus, coba dashmisalnya. Lihat juga unix.stackexchange.com/q/209123/38906
cuonglm

Jawaban:

256

Ya, kami melihat sejumlah hal seperti:

while read line; do
  echo $line | cut -c3
done

Atau lebih buruk:

for line in `cat file`; do
  foo=`echo $line | awk '{print $2}'`
  echo whatever $foo
done

(jangan tertawa, saya sudah melihat banyak dari mereka).

Umumnya dari pemula shell scripting. Itu adalah terjemahan harfiah yang naif dari apa yang akan Anda lakukan dalam bahasa imperatif seperti C atau python, tapi itu bukan cara Anda melakukan hal-hal dalam shell, dan contoh-contoh itu sangat tidak efisien, sama sekali tidak dapat diandalkan (berpotensi menyebabkan masalah keamanan), dan jika Anda pernah mengelola untuk memperbaiki sebagian besar bug, kode Anda menjadi tidak terbaca.

Secara konseptual

Di C atau sebagian besar bahasa lain, blok bangunan hanya satu tingkat di atas instruksi komputer. Anda memberi tahu prosesor Anda apa yang harus dilakukan dan kemudian apa yang harus dilakukan selanjutnya. Anda mengambil prosesor Anda dengan tangan dan mengelolanya: Anda membuka file itu, Anda membaca banyak byte, Anda melakukan ini, Anda melakukannya dengan itu.

Kerang adalah bahasa tingkat yang lebih tinggi. Orang mungkin mengatakan itu bahkan bukan bahasa. Mereka di depan semua penerjemah baris perintah. Pekerjaan dilakukan oleh perintah-perintah yang Anda jalankan dan shell hanya dimaksudkan untuk mengaturnya.

Salah satu hal hebat yang diperkenalkan Unix adalah pipa dan aliran stdin / stdout / stderr default yang ditangani semua perintah secara default.

Dalam 45 tahun, kami tidak menemukan yang lebih baik dari API untuk memanfaatkan kekuatan perintah dan meminta mereka bekerja sama untuk suatu tugas. Itu mungkin alasan utama mengapa orang masih menggunakan kerang saat ini.

Anda punya alat pemotong dan alat transliterasi, dan Anda bisa melakukannya:

cut -c4-5 < in | tr a b > out

Shell hanya melakukan pipa ledeng (membuka file, mengatur pipa, menjalankan perintah) dan ketika semuanya siap, itu hanya mengalir tanpa shell melakukan apa pun. Alat melakukan pekerjaan mereka secara bersamaan, efisien dengan kecepatan mereka sendiri dengan buffering yang cukup sehingga tidak ada yang menghalangi yang lain, itu hanya indah dan sederhana.

Meminta alat memiliki biaya (dan kami akan mengembangkannya pada titik kinerja). Alat-alat itu dapat ditulis dengan ribuan instruksi dalam C. Suatu proses harus dibuat, alat itu harus dimuat, diinisialisasi, kemudian dibersihkan, proses dihancurkan dan menunggu.

Memohon cutseperti membuka laci dapur, mengambil pisau, menggunakannya, mencuci, mengeringkannya, memasukkannya kembali ke dalam laci. Saat kamu melakukan:

while read line; do
  echo $line | cut -c3
done < file

Ini seperti untuk setiap baris file, mendapatkan readalat dari laci dapur (yang sangat canggung karena tidak dirancang untuk itu ), membaca baris, mencuci alat baca Anda, memasukkannya kembali ke dalam laci. Kemudian jadwalkan pertemuan untuk echodan cutalat, ambil dari laci, panggil mereka, cuci, keringkan, masukkan kembali ke dalam laci dan seterusnya.

Beberapa alat tersebut ( readdan echo) dibangun di sebagian besar shell, tapi itu hampir tidak membuat perbedaan di sini karena echodan cutmasih perlu dijalankan dalam proses terpisah.

Ini seperti memotong bawang tetapi mencuci pisau Anda dan memasukkannya kembali ke laci dapur di antara setiap irisan.

Di sini cara yang jelas adalah mengambil cutalat Anda dari laci, mengiris bawang Anda dan memasukkannya kembali ke dalam laci setelah seluruh pekerjaan selesai.

TKI, dalam shell, terutama untuk memproses teks, Anda memanggil utilitas sesedikit mungkin dan meminta mereka bekerja sama untuk tugas tersebut, tidak menjalankan ribuan alat secara berurutan menunggu masing-masing untuk memulai, menjalankan, membersihkan sebelum menjalankan yang berikutnya.

Bacaan lebih lanjut dalam jawaban baik Bruce . Alat internal pemrosesan teks tingkat rendah dalam shell (kecuali mungkin untuk zsh) terbatas, rumit, dan umumnya tidak cocok untuk pemrosesan teks umum.

Performa

Seperti yang dikatakan sebelumnya, menjalankan satu perintah memiliki biaya. Biaya besar jika perintah itu tidak dibangun, tetapi bahkan jika mereka dibangun, biayanya besar.

Dan shell tidak dirancang untuk berjalan seperti itu, mereka tidak memiliki pretensi untuk menjadi bahasa pemrograman yang performant. Mereka bukan, mereka hanya penafsir baris perintah. Jadi, sedikit optimasi yang telah dilakukan di bagian depan ini.

Juga, shell menjalankan perintah dalam proses terpisah. Blok bangunan tersebut tidak berbagi memori atau keadaan umum. Ketika Anda melakukan a fgets()atau fputs()di C, itu adalah fungsi di stdio. stdio menyimpan buffer internal untuk input dan output untuk semua fungsi stdio, untuk menghindari terlalu sering melakukan panggilan sistem yang mahal.

Yang sesuai bahkan builtin shell utilitas ( read, echo, printf) tidak bisa melakukan itu. readdimaksudkan untuk membaca satu baris. Jika terbaca melewati karakter baris baru, itu berarti perintah berikutnya yang Anda jalankan akan melewatkannya. Jadi readharus membaca input satu byte pada satu waktu (beberapa implementasi memiliki optimasi jika input adalah file biasa karena mereka membaca potongan dan mencari kembali, tetapi itu hanya bekerja untuk file biasa dan bashmisalnya hanya membaca 128 byte potongan yang merupakan masih jauh lebih sedikit daripada yang akan dilakukan utilitas teks).

Sama di sisi output, echotidak bisa hanya buffer output, itu harus langsung output karena perintah berikutnya yang Anda jalankan tidak akan berbagi buffer itu.

Jelas, menjalankan perintah secara berurutan berarti Anda harus menunggu untuk itu, itu adalah tarian scheduler kecil yang memberikan kontrol dari shell dan ke alat dan kembali. Itu juga berarti (tidak seperti menggunakan alat contoh yang berjalan lama dalam pipa) bahwa Anda tidak dapat memanfaatkan beberapa prosesor pada saat yang sama saat tersedia.

Antara while readloop dan setara (seharusnya) cut -c3 < file, dalam tes cepat saya, ada rasio waktu CPU sekitar 40000 dalam tes saya (satu detik versus setengah hari). Tetapi bahkan jika Anda hanya menggunakan shell builtin:

while read line; do
  echo ${line:2:1}
done

(di sini dengan bash), itu masih sekitar 1: 600 (satu detik vs 10 menit).

Keandalan / keterbacaan

Sangat sulit untuk mendapatkan kode itu dengan benar. Contoh yang saya berikan terlihat terlalu sering di alam liar, tetapi mereka memiliki banyak bug.

readadalah alat praktis yang dapat melakukan banyak hal berbeda. Itu dapat membaca input dari pengguna, membaginya menjadi kata-kata untuk menyimpan dalam variabel yang berbeda. read linetidak tidak membaca garis masukan, atau mungkin membaca garis dengan cara yang sangat khusus. Ini sebenarnya membaca kata-kata dari input kata-kata yang dipisahkan oleh $IFSdan di mana backslash dapat digunakan untuk melarikan diri dari pemisah atau karakter baris baru.

Dengan nilai default $IFS, pada input seperti:

   foo\/bar \
baz
biz

read lineakan menyimpan "foo/bar baz"ke dalam $line, tidak " foo\/bar \"seperti yang Anda harapkan.

Untuk membaca sebuah baris, Anda sebenarnya perlu:

IFS= read -r line

Itu tidak terlalu intuitif, tapi memang begitu, ingat kerang tidak dimaksudkan untuk digunakan seperti itu.

Sama untuk echo. echomemperluas urutan. Anda tidak dapat menggunakannya untuk konten sewenang-wenang seperti konten file acak. Kamu butuh di printfsini sebagai gantinya.

Dan tentu saja, ada yang khas lupa mengutip variabel Anda yang semua orang jatuh ke dalamnya. Jadi lebih dari itu:

while IFS= read -r line; do
  printf '%s\n' "$line" | cut -c3
done < file

Sekarang, beberapa peringatan lagi:

  • kecuali zsh, itu tidak berfungsi jika input berisi karakter NUL sementara setidaknya utilitas teks GNU tidak akan memiliki masalah.
  • jika ada data setelah baris baru terakhir, itu akan dilewati
  • di dalam loop, stdin diarahkan sehingga Anda perlu memperhatikan bahwa perintah di dalamnya tidak membaca dari stdin.
  • untuk perintah dalam loop, kami tidak memperhatikan apakah mereka berhasil atau tidak. Biasanya, kondisi kesalahan (disk penuh, kesalahan baca ...) akan ditangani dengan buruk, biasanya lebih buruk daripada dengan yang setara yang benar .

Jika kami ingin mengatasi beberapa masalah di atas, itu menjadi:

while IFS= read -r line <&3; do
  {
    printf '%s\n' "$line" | cut -c3 || exit
  } 3<&-
done 3< file
if [ -n "$line" ]; then
    printf '%s' "$line" | cut -c3 || exit
fi

Itu menjadi semakin tidak terbaca.

Ada sejumlah masalah lain dengan mengirimkan data ke perintah melalui argumen atau mengambil hasilnya dalam variabel:

  • pembatasan ukuran argumen (beberapa implementasi utilitas teks memiliki batas di sana juga, meskipun efek yang dicapai umumnya kurang bermasalah)
  • karakter NUL (juga masalah dengan utilitas teks).
  • argumen diambil sebagai opsi saat mereka memulai dengan -(atau +terkadang)
  • berbagai kebiasaan berbagai perintah yang biasanya digunakan dalam loop seperti expr, test...
  • operator manipulasi teks (terbatas) dari berbagai shell yang menangani karakter multi-byte dengan cara yang tidak konsisten.
  • ...

Pertimbangan keamanan

Saat Anda mulai bekerja dengan variabel shell dan argumen untuk perintah , Anda memasukkan bidang ranjau.

Jika Anda lupa mengutip variabel Anda , lupakan akhir dari opsi penanda , bekerja di lokal dengan karakter multi-byte (norma hari ini), Anda pasti akan memperkenalkan bug yang cepat atau lambat akan menjadi kerentanan.

Ketika Anda mungkin ingin menggunakan loop.

TBD

Stéphane Chazelas
sumber
24
Jelas (jelas), mudah dibaca dan sangat membantu. Terima kasih sekali lagi. Ini sebenarnya penjelasan terbaik yang saya lihat di mana saja di internet untuk perbedaan mendasar antara skrip shell dan pemrograman.
Wildcard
2
Posting seperti inilah yang membantu pemula belajar tentang Shell Script dan melihat perbedaannya yang halus. Seharusnya menambahkan variabel referensi sebagai $ {VAR: -default_value} untuk memastikan Anda tidak mendapatkan nol. dan set -o nounset untuk membentak Anda saat mereferensikan nilai yang tidak didefinisikan.
unsignedzero
6
@ A.Danischewski, saya pikir Anda tidak mengerti intinya. Ya cutmisalnya efisien. cut -f1 < a-very-big-fileefisien, seefisien yang akan Anda dapatkan jika Anda menulisnya dalam C. Apa yang sangat tidak efisien dan rawan kesalahan adalah memohon cutuntuk setiap baris a-very-big-filedalam shell loop yang merupakan titik yang dibuat dalam jawaban ini. Itu sesuai dengan pernyataan terakhir Anda tentang menulis kode yang tidak perlu yang membuat saya berpikir mungkin saya tidak mengerti komentar Anda.
Stéphane Chazelas
5
"Dalam 45 tahun, kami belum menemukan yang lebih baik dari API untuk memanfaatkan kekuatan perintah dan meminta mereka bekerja sama untuk suatu tugas." - sebenarnya, PowerShell, misalnya, telah memecahkan masalah parsing yang ditakuti dengan memberikan data terstruktur daripada stream byte. Satu-satunya alasan cangkang belum menggunakannya (idenya telah ada cukup lama dan pada dasarnya telah mengkristal di sekitar Jawa ketika jenis-jenis daftar kamus & wadah saat ini menjadi arus utama) adalah pengelola mereka belum dapat menyetujui format data terstruktur umum untuk digunakan (.
ivan_pozdeev
6
@OlivierDulac Saya pikir itu sedikit humor. Bagian itu akan menjadi TBD selamanya.
muru
43

Sejauh konseptual dan keterbacaan berjalan, kerang biasanya tertarik pada file. "Unit yang dapat dialamatkan" adalah file, dan "alamat" adalah nama file. Shells memiliki semua jenis metode pengujian untuk keberadaan file, tipe file, pemformatan nama file (dimulai dengan globbing). Shell memiliki sedikit primitif untuk menangani konten file. Pemrogram Shell harus menjalankan program lain untuk menangani konten file.

Karena orientasi file dan nama file, melakukan manipulasi teks di shell benar-benar lambat, seperti yang telah Anda catat, tetapi juga memerlukan gaya pemrograman yang tidak jelas dan berkerut.

Bruce Ediger
sumber
25

Ada beberapa jawaban yang rumit, memberikan banyak detail menarik bagi para Geeks di antara kita, tetapi itu benar-benar sangat sederhana - memproses file besar dalam sebuah shell loop terlalu lambat.

Saya pikir si penanya menarik dalam jenis skrip shell yang khas, yang mungkin dimulai dengan beberapa penguraian baris perintah, pengaturan lingkungan, memeriksa file dan direktori, dan sedikit lebih banyak inisialisasi, sebelum melanjutkan ke pekerjaan utamanya: melalui pekerjaan besar: file teks berorientasi baris.

Untuk bagian pertama ( initialization), biasanya perintah shell tidak lambat - hanya menjalankan beberapa lusin perintah, mungkin dengan beberapa loop pendek. Bahkan jika kita menulis bagian itu secara tidak efisien, biasanya akan memakan waktu kurang dari sedetik untuk melakukan semua inisialisasi itu, dan itu tidak masalah - itu hanya terjadi sekali.

Tetapi ketika kita mulai memproses file besar, yang bisa memiliki ribuan atau jutaan baris, itu tidak baik untuk skrip shell untuk mengambil sebagian kecil dari yang kedua (bahkan jika itu hanya beberapa lusin milidetik) untuk setiap baris, karena itu bisa menambah hingga berjam-jam.

Saat itulah kita perlu menggunakan alat lain, dan keindahan skrip shell Unix adalah mereka membuatnya sangat mudah bagi kita untuk melakukan itu.

Alih-alih menggunakan loop untuk melihat setiap baris, kita perlu melewatkan seluruh file melalui pipa perintah . Ini berarti, alih-alih memanggil perintah ribuan atau jutaan waktu, shell memanggilnya hanya sekali. Memang benar bahwa perintah-perintah itu akan memiliki loop untuk memproses file baris demi baris, tetapi mereka bukan skrip shell dan mereka dirancang untuk menjadi cepat dan efisien.

Unix memiliki banyak alat bawaan yang canggih, mulai dari yang sederhana hingga yang kompleks, yang dapat kita gunakan untuk membangun jaringan pipa kami. Saya biasanya mulai dengan yang sederhana, dan hanya menggunakan yang lebih kompleks bila perlu.

Saya juga akan mencoba untuk tetap dengan alat standar yang tersedia di sebagian besar sistem, dan mencoba untuk menjaga penggunaan portabel saya, meskipun itu tidak selalu mungkin. Dan jika bahasa favorit Anda adalah Python atau Ruby, mungkin Anda tidak akan keberatan dengan upaya ekstra untuk memastikan itu diinstal pada setiap platform yang harus dijalankan oleh perangkat lunak Anda :-)

Alat-alat sederhana termasuk head, tail, grep, sort, cut, tr, sed, join(ketika penggabungan 2 file), dan awksatu-liners, di antara banyak lainnya. Sungguh menakjubkan apa yang bisa dilakukan beberapa orang dengan pencocokan pola dan sedperintah.

Ketika menjadi lebih kompleks, dan Anda benar-benar harus menerapkan beberapa logika untuk setiap baris, awkadalah pilihan yang baik - baik satu-liner (beberapa orang meletakkan skrip awk keseluruhan dalam 'satu baris', meskipun itu tidak terlalu mudah dibaca) atau dalam skrip eksternal pendek.

Seperti awkbahasa yang ditafsirkan (seperti cangkang Anda), sungguh menakjubkan bahwa ia dapat melakukan pemrosesan baris demi baris dengan sangat efisien, tetapi dibuat khusus untuk ini dan sangat cepat.

Dan kemudian ada Perldan sejumlah besar bahasa scripting lain yang sangat bagus dalam memproses file teks, dan juga datang dengan banyak perpustakaan yang bermanfaat.

Dan akhirnya, ada C lama yang bagus, jika Anda membutuhkan kecepatan maksimum dan fleksibilitas tinggi (walaupun pemrosesan teks agak membosankan). Tapi itu mungkin penggunaan waktu Anda yang sangat buruk untuk menulis program C baru untuk setiap tugas pemrosesan file yang berbeda yang Anda temui. Saya banyak bekerja dengan file CSV, jadi saya telah menulis beberapa utilitas umum dalam C yang dapat saya gunakan kembali di banyak proyek yang berbeda. Akibatnya, ini memperluas jangkauan 'alat Unix yang sederhana dan cepat' yang dapat saya panggil dari skrip shell saya, jadi saya dapat menangani sebagian besar proyek hanya dengan menulis skrip, yang jauh lebih cepat daripada menulis dan men-debug kode C yang dipesan lebih dahulu setiap kali!

Beberapa petunjuk terakhir:

  • jangan lupa untuk memulai skrip shell utama Anda export LANG=C, atau banyak alat akan memperlakukan file ASCII polos-lama Anda sebagai Unicode, membuatnya jauh lebih lambat
  • pertimbangkan juga pengaturan export LC_ALL=Cjika Anda ingin sortmenghasilkan pemesanan yang konsisten, terlepas dari lingkungannya!
  • jika Anda perlu sortdata Anda, itu mungkin akan membutuhkan lebih banyak waktu (dan sumber daya: CPU, memori, disk) daripada yang lainnya, jadi cobalah untuk meminimalkan jumlah sortperintah dan ukuran file yang mereka sortir
  • satu saluran pipa, bila memungkinkan, biasanya paling efisien - menjalankan beberapa saluran pipa secara berurutan, dengan file-file perantara, mungkin lebih mudah dibaca dan dapat di-debug, tetapi akan menambah waktu yang dibutuhkan program Anda
Laurence Renshaw
sumber
6
Pipa banyak alat sederhana (khususnya yang disebutkan, seperti kepala, ekor, grep, sortir, potong, tr, sed, ...) sering digunakan secara tidak perlu, khususnya jika Anda juga memiliki contoh awk dalam pipa yang dapat melakukan tugas alat-alat sederhana itu juga. Masalah lain yang perlu dipertimbangkan adalah bahwa dalam saluran pipa Anda tidak bisa dengan sederhana dan andal menyampaikan informasi keadaan dari proses di sisi depan pipa ke proses yang muncul di sisi belakang. Jika Anda menggunakan pipelines program sederhana seperti program awk, Anda memiliki ruang status tunggal.
Janis
14

Ya tapi...

The jawaban yang benar dari Stéphane Chazelas didasarkan pada konsep mendelegasikan setiap operasi teks ke binari tertentu, seperti grep, awk, seddan lain-lain.

Karena mampu melakukan banyak hal sendiri, menjatuhkan garpu dapat menjadi lebih cepat (bahkan daripada menjalankan penerjemah lain untuk melakukan semua pekerjaan).

Untuk contoh, lihat posting ini:

https://stackoverflow.com/a/38790442/1765658

dan

https://stackoverflow.com/a/7180078/1765658

uji dan bandingkan ...

Tentu saja

Tidak ada pertimbangan tentang input dan keamanan pengguna !

Jangan menulis aplikasi web di bawah !!

Tetapi untuk banyak tugas administrasi server, di mana dapat digunakan sebagai pengganti , menggunakan bash bawaan bisa sangat efisien.

Arti saya:

Alat tulis seperti bin utils bukan jenis pekerjaan yang sama dengan administrasi sistem.

Jadi bukan orang yang sama!

Di mana sysadmin harus tahu shell, mereka dapat menulis prototipe dengan menggunakan alat yang lebih disukai (dan paling dikenal).

Jika utilitas baru ini (prototipe) benar-benar bermanfaat, beberapa orang lain dapat mengembangkan alat khusus dengan menggunakan beberapa bahasa yang lebih tepat.

F. Hauri
sumber
1
Contoh yang baik. Pendekatan Anda tentu lebih efisien daripada lololux satu, tetapi perhatikan bagaimana jawaban tensibai (cara yang tepat untuk melakukan IMO ini, yaitu tanpa menggunakan loop shell) adalah urutan besarnya lebih cepat dari Anda. Dan milik Anda jauh lebih cepat jika Anda tidak menggunakannya bash. (Lebih dari 3 kali lebih cepat dengan ksh93 dalam pengujian saya pada sistem saya). bashumumnya shell paling lambat. Bahkan zshdua kali lebih cepat pada skrip itu. Anda juga memiliki beberapa masalah dengan variabel tanda kutip dan penggunaan read. Jadi, Anda sebenarnya menggambarkan banyak poin saya di sini.
Stéphane Chazelas
@ StéphaneChazelas Saya setuju, bash mungkin adalah shell paling lambat yang bisa digunakan orang saat ini, tetapi yang paling banyak digunakan.
F. Hauri
@ StéphaneChazelas Saya telah memposting versi perl pada jawaban saya
F. Hauri
1
@Tensibai, Anda akan menemukan POSIXsh , Awk , Sed , grep, ed, ex, cut, sort, join... semua dengan kehandalan lebih dari Bash atau Perl.
Wildcard
1
@Tensibai, dari semua sistem yang terkait dengan U&L, kebanyakan dari mereka (Solaris, FreeBSD, HP / UX, AIX, sebagian besar sistem Linux yang disematkan ...) tidak disertai dengan bashinstal secara default. bashsebagian besar hanya ditemukan di Apple MacOS dan sistem GNU (Saya kira bahwa apa yang Anda sebut distro utama ), meskipun banyak sistem juga memiliki sebagai paket opsional (seperti zsh, tcl, python...)
Stéphane Chazelas