Metni işlemek için neden bir kabuk halkası kullanılmıyor?


196

POSIX kabuklarında metni genel olarak kötü bir uygulama olarak kabul etmek için bir süre döngüsü kullanıyor mu?

Stéphane Chazelas'ın belirttiği gibi , kabuk halkasını kullanmama nedenlerinden bazıları kavramsal , güvenilirlik , okunaklılık , performans ve güvenliktir .

Bu cevap güvenilirlik ve okunaklılık yönlerini açıklar :

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

İçin performans , whiledöngü ve okuma bir dosya veya bir borudan okurken, çünkü müthiş yavaş okuma kabuk dahili bir seferde bir karakter okur.

Kavramsal ve güvenlik yönleri nasıl ?


İlgili (madalyonun diğer tarafı): Dosyaya bu kadar çabuk nasıl yesyazılır?
Joker

1
Dahili okuma kabuğu bir defada tek bir karakter okumaz, bir seferde tek bir satır okur. wiki.bash-hackers.org/commands/builtin/read
A.Danischewski

@ A.Danischewski: Bu kabuğuna bağlıdır. In bash, bir kerede bir tampon boyutunu okur, dashörneğin deneyin . Ayrıca bakınız unix.stackexchange.com/q/209123/38906
cuonglm

Yanıtlar:


256

Evet, şöyle bir dizi şey görüyoruz:

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

Ya da daha kötüsü:

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

(gülme, onlardan çok gördüm).

Genellikle kabuk komut dosyası başlayanlar. Bunlar, C veya python gibi zorunlu dillerde yapabileceğinizlerin tam anlamıyla çevirileridir, ancak mermilerdeki işleri böyle yapmazsınız ve bu örnekler çok verimsizdir, tamamen güvenilmezdir (potansiyel olarak güvenlik sorunlarına yol açar) ve yönetirseniz hataların çoğunu gidermek için kodunuz okunaksız hale gelir.

kavramsal olarak

C veya diğer birçok dilde, yapı taşları bilgisayar talimatlarının sadece bir üstündedir. İşlemcinize ne yapacağınızı ve sonra ne yapacağınızı söyleyin. İşlemcinizi elinize alıyorsunuz ve mikro-yönetiyorsunuz: o dosyayı açıyorsunuz, birçok baytı okuyorsunuz, bunu yapıyorsunuz, onunla yapıyorsunuz.

Kabuklar daha yüksek bir dildir. Bir dilin bile olmadığını söyleyebilir. Tüm komut satırı tercümanlarından önce. İş, çalıştırdığınız komutlar tarafından yapılır ve kabuk sadece onları düzenlemek içindir.

Unix'in sunduğu harika şeylerden biri boru ve varsayılan olarak tüm komutların işlediği varsayılan stdin / stdout / stderr akışlarıydı.

45 yılda, komutların gücünden faydalanmak ve bir göreve işbirliği yapmalarını sağlamak için bu API'den daha iyi bir şey bulamadık. Bu muhtemelen insanların bugün hala mermi kullanmasının ana nedeni.

Bir kesme aletiniz ve bir harf çevirisi aletiniz var ve şunları yapabilirsiniz:

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

Kabuk sadece sıhhi tesisat yapıyor (dosyaları açın, boruları ayarlayın, komutları çağırın) ve hepsi hazır olduğunda, kabuğun hiçbir şey yapmadan akması sağlanır. Araçlar işlerini eşzamanlı, verimli bir şekilde, diğerini bloke etmemek için yeterli tamponlama ile kendi hızlarında yaparlar, sadece güzel ve çok basit.

Bir aracın kullanılmasının bir maliyeti vardır (ve bunu performans noktasında geliştireceğiz). Bu araçlar C'deki binlerce komutla yazılabilir. Bir işlem yaratılmalı, alet yüklenmeli, kullanıma hazır hale getirilmeli, daha sonra temizlenmeli, işlem imha edilmeli ve beklenmelidir.

Davet etmek cut, mutfak çekmecesini açmak, bıçağı almak, kullanmak, yıkamak, kurutmak, tekrar çekmeceye koymak gibidir. Ne zaman yaparsın:

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

Dosyanın her satırında olduğu gibi, readaleti mutfak çekmecesinden almak (çok sakarca çünkü bunun için tasarlanmamıştır ), çizgiyi okumak, okuma aletinizi yıkamak, çekmeceye koymak. Sonra echove cutaraç için bir toplantı planlayın , çekmeceden çıkarın, onları çağırın, yıkayın, kurutun, çekmeceye geri koyun ve benzeri.

Bu araçlardan bazıları ( readve echo) çoğu kabukta inşa edilmiştir, ancak bu, o zamandan beri fark yaratmaz echove cutayrı işlemlerde çalıştırılması gerekir.

Bir soğanı kesmek gibi ama bıçağınızı yıkamak ve her dilim arasında tekrar mutfak çekmecesine koymak gibi.

Burada bariz yol, cutaletinizi çekmeceden almak, bütün soğanı dilimlemek ve tüm iş bittikten sonra çekmeceye geri koymaktır.

IOW, kabuklarda, özellikle de metni işlemek için, mümkün olduğunca az yardımcı program başlattınız ve bir sonrakiini çalıştırmadan önce her birinin başlamasını, çalıştırılmasını ve temizlenmesini bekleyen sıralı binlerce aracı çalıştırmamalarını sağlayın.

Bruce'un iyi cevabında daha fazla okuma . Kabuklardaki düşük seviyeli metin işleme dahili araçları (belki de hariç zsh) sınırlıdır, zahmetlidir ve genellikle genel metin işlemesi için uygun değildir.

Verim

Daha önce de belirtildiği gibi, bir komut çalıştırmanın bir maliyeti vardır. Bu komut yerleşik değilse büyük bir maliyet, ancak yerleşik olsalar bile, maliyet büyüktür.

Ve kabuklar bu şekilde çalışacak şekilde tasarlanmadı, performans programlama dilleri yapmayı önemsemediler. Onlar değil, sadece komut satırı tercümanları. Böylece bu cephede çok az optimizasyon yapıldı.

Ayrıca, kabukları komutları ayrı işlemlerde çalıştırır. Bu yapı taşları ortak bir hafızayı veya durumu paylaşmaz. Bir fgets()veya fputs()C'de yaptığınız zaman , bu stdio'daki bir fonksiyondur. stdio, pahalı sistem çağrılarını çok sık yapmaktan kaçınmak için, tüm stdio işlevleri için giriş ve çıkış için dahili tamponları tutar.

Karşılık gelen daha yerleşik kabuk araçları ( read, echo, printf) bunu yapamaz. readbir satır okumak içindir. Newline karakterini okursa, çalıştırdığınız bir sonraki komut onu kaçıracaktır. Bu nedenle read, girişi bir defada bir bayt okumak zorundadır (bazı uygulamalar girdilerin topakları okudukları ve geri aradıkları normal bir dosya ise optimizasyonu vardır, ancak bu sadece normal dosyalar için çalışır ve bashörneğin sadece 128 baytlık parçalar okur). hala metin yardımcı programlarının yapabileceğinden çok daha az.

Çıktı tarafında aynı, echosadece çıktısını tamponlayamaz, hemen çalıştırması gerekir çünkü çalıştırdığınız bir sonraki komut bu arabelleği paylaşmaz.

Açıkçası, komutları sırayla çalıştırmak, onları beklemeniz gerektiği anlamına gelir, bu kabuktan ve aletlerden ve sırttan kontrol sağlayan küçük bir zamanlayıcı dansıdır. Bu aynı zamanda (bir boru hattında uzun süre çalışan aletlerin kullanılmasının tersine), mümkün olduğunda birkaç işlemciyi aynı anda kullanamayacağınız anlamına gelir.

Bu while readdöngü ve (sözde) eşdeğeri cut -c3 < filearasında hızlı testimde testlerimde 40000 civarında bir CPU zaman oranı var (bir saniye ile yarım gün arasında). Ancak sadece kabuk yapıları kullanıyor olsanız bile:

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

(burada ile bash), hala 1: 600 civarında (bir saniye vs 10 dakika).

Güvenilirlik / okunabilirlik

Bu kodu doğru yapmak çok zor. Verdiğim örnekler vahşi doğada çok sık görülüyor ancak birçok böcekleri var.

readbirçok farklı şey yapabilen kullanışlı bir araçtır. Kullanıcıdan girdi okuyabilir, farklı değişkenlerde saklamak için kelimelere bölebilir. read lineyok değil girişin bir çizgi okumak, ya da belki çok özel bir şekilde bir çizgi okur. Aslında okur kelimeleri girişten ile ayrılan bu kelimeleri $IFSve nerede ters eğik çizgi ayırıcılar veya satır karakteri kaçmak için kullanılabilir.

Varsayılan olarak $IFS, gibi bir girişte:

   foo\/bar \
baz
biz

read linebeklediğiniz gibi değil , "foo/bar baz"içine saklayacağım .$line" foo\/bar \"

Bir satır okumak için, aslında:

IFS= read -r line

Bu çok sezgisel değil, ama bu şekilde, kabukları böyle kullanılmadığını hatırla.

İçin aynı echo. echodizileri genişletir. Rasgele bir dosyanın içeriği gibi rastgele içerikler için kullanamazsınız. Bunun printfyerine buraya ihtiyacınız var.

Ve elbette, herkesin içine düştüğü değişkenden alıntı yapmanın tipik bir unutması var . Yani daha fazlası:

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

Şimdi, birkaç uyarı daha:

  • Bunun dışında zsh, giriş NUL karakterleri içeriyorsa, bu en azından GNU metin programları sorun yaşamazsa işe yaramaz.
  • Son satırdan sonra veri varsa, atlanacaktır.
  • döngü içinde, stdin yeniden yönlendirilir, bu nedenle içindeki komutların stdin'den okumamasına dikkat etmeniz gerekir.
  • döngüler içindeki komutlar için başarılı olup olmadıklarına dikkat etmiyoruz. Genellikle, hata (disk dolu, okuma hataları ...) koşulları, genellikle doğru eşdeğerden daha zayıf olarak, kötü ele alınacaktır .

Yukarıdaki bu sorunlardan bazılarını ele almak istiyorsak, bu olur:

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

Bu giderek daha az okunaklı hale geliyor.

Verileri argümanlarla komutlara geçirmenin veya değişkenlerde çıktılarını almanın bir kaç sorunu var:

  • Argümanların büyüklüğü üzerindeki sınırlama (bazı metin yardımcı programları uygulamalarında da bir sınırlama vardır, ancak ulaşılanların etkisi genellikle daha az problemlidir)
  • NUL karakteri (ayrıca metin yardımcı programlarıyla ilgili bir sorun).
  • başladığında -(veya +bazen) seçenek olarak alınan argümanlar
  • tipik gibilerin döngüler kullanılan çeşitli komutların çeşitli tuhaflıklar expr, test...
  • çok baytlık karakterleri tutarsız şekillerde işleyen çeşitli kabukların (sınırlı) metin işleme operatörleri.
  • ...

Güvenlik Hususları

Komutlara kabuk değişkenleri ve argümanlar ile çalışmaya başladığınızda , bir mayın tarlasına girersiniz.

Değişkenlerinizden alıntı yapmayı unutursanız , seçenek işaretleyicinin sonunu unutun, çok baytlık karakterlerle yerel olarak çalışın (bugünlerde norm), er ya da geç kırılganlık yaratacak hatalar getireceğiniz kesin.

Ne zaman döngüler kullanmak isteyebilirsiniz.

TBD


24
Açık (canlı), okunabilir ve son derece yararlı. Tekrar teşekkürler. Bu aslında internet üzerinde herhangi bir yerde gördüğüm en iyi açıklama. Kabuk yazımı ve programlama arasındaki temel fark.
Wildcard

2
Yeni başlayanların Shell Script'lerini öğrenmelerine ve ince farkları görmelerine yardımcı olan bu yazılar. Boş almadığınızdan emin olmak için referans değişkenini $ {VAR: -default_value} olarak eklemelidir. ve -o nounset değerini, tanımlanmamış bir değere referans verirken size bağırmak üzere ayarlayın.
unsignedzero

6
@ A.Danischewski, noktayı kaçırdığınızı düşünüyorum. Evet cut, örneğin verimli. cut -f1 < a-very-big-fileC'ye yazarsanız elde edebileceğiniz kadar verimlidir. Verimli olmayan ve hataya açık olan şey, bu cevabın yapıldığı nokta olan bir kabuk döngüsünün cuther satırı için çağrıştırıcıdır a-very-big-file. Bu, gereksiz kod yazmayla ilgili son ifadenizle aynı fikirdedir; bu da beni yorumunuzu anlamadığımı düşündürüyor.
Stéphane Chazelas

5
“45 yıl içinde, komutların gücünden faydalanmak ve bir göreve işbirliği yapmalarını sağlamak için bu API'den daha iyi bir şey bulamadık.” - aslında, PowerShell, birincisi, korkunç ayrıştırma problemini bayt akışlarından ziyade yapılandırılmış veriyi dolaşarak çözdü. Tek nedeni kabukları henüz kullanmıyor (fikir uzun süredir oradaydı ve şu anda standart bir liste ve sözlük kabı türleri ana akım haline geldiğinde temelde Java etrafında kristalleşti.) Ortak yapılandırılmış veri formatı kullanılacak (.
ivan_pozdeev

6
@ OliverDulac Bu biraz mizah olduğunu düşünüyorum. Bu bölüm sonsuza kadar TBD olacak.
muru

43

Kavramsal ve okunaklılık söz konusu olduğunda, kabukları tipik olarak dosyalarla ilgilenir. Onların "adreslenebilir birimi" dosya, "adres" ise dosya adıdır. Kabuklarda dosya mevcudiyeti, dosya tipi, dosya adı formatlaması (globbing ile başlar) için her türlü test yöntemi vardır. Kabukların dosya içerikleriyle ilgilenmek için çok az ilkeleri vardır. Shell programcıları, dosya içerikleriyle ilgilenmek için başka bir programı çağırmak zorundadır.

Dosya ve dosya adı yönlendirme nedeniyle, kabukta metin manipülasyonu yapmak, belirttiğiniz gibi gerçekten yavaştır, ancak aynı zamanda net olmayan ve çarpıtılmış bir programlama stili gerektirir.


25

Aramızdaki meraklılar için birçok ilginç ayrıntı veren, bazı karmaşık cevaplar var, ancak gerçekten çok basit - büyük bir dosyayı kabuk döngüsünde işlemek çok yavaş.

Bence asıl işe başlamadan önce, sorgulayıcı bazı komut satırı ayrıştırma, ortam ayarlama, dosya ve dizinleri kontrol etme ve biraz daha başlangıç ​​durumuna getirme ile başlayabilen tipik bir kabuk senaryosunda ilginçtir. satır odaklı metin dosyası.

İlk bölümlerde ( initialization), genellikle kabuk komutlarının yavaş olması önemli değildir - sadece birkaç düzine komut çalıştırır, belki birkaç kısa döngüyle. Bu kısmı verimli bir şekilde yazmasak bile, tüm bu başlatma işlemlerini yapmak genellikle bir saniyeden daha az zaman alır ve bu iyidir - bu sadece bir kez olur.

Fakat binlerce ya da milyonlarca satırdan oluşan büyük dosyayı işlemeye başladığımızda , kabuk betiğinin her satır için bir saniyenin (sadece birkaç düzine milisaniyede olsa bile) saniyenin önemli bir bölümünü alması iyi olmaz. Bu saate kadar sürebilir.

O zaman başka araçları kullanmamız gerekiyor ve Unix'in kabuk betiklerinin güzelliği, bunu yapmamızı çok kolaylaştırıyor.

Her satıra bakmak için bir döngü kullanmak yerine, tüm dosyayı bir komut satırından geçirmemiz gerekir . Bu, komutları binlerce veya milyonlarca kez çağırmak yerine, kabuğun yalnızca bir kez çağırdığı anlamına gelir. Bu komutların dosyayı satır satır işlemek için döngülere sahip olduğu doğrudur, ancak kabuk komut dosyaları değildir ve hızlı ve verimli olacak şekilde tasarlanmıştır.

Unix, boru hattımızı oluşturmak için kullanabileceğimiz basitten karmaşığa uzanan birçok harika araçlara sahiptir. Genelde basit olanlardan başlarım ve gerektiğinde sadece daha karmaşık olanları kullanırdım.

Ayrıca, çoğu sistemde bulunan standart araçlarla takılmaya çalışırdım ve her zaman mümkün olmasa da kullanımımı taşınabilir tutmaya çalışırdım. En sevdiğiniz dil Python veya Ruby ise, belki de yazılımınızın çalışması gereken her platforma yüklenmesini sağlama çabalarını dikkate almazsınız.

Basit araçlar şunlardır head, tail, grep, sort, cut, tr, sed, join(2 dosya birleştirirken) ve awkdiğerleri arasında tek gömlekleri. Bazı insanların kalıp eşleştirme ve sedkomutlarla neler yapabildikleri şaşırtıcı .

Daha karmaşık hale geldiğinde ve her satıra biraz mantık uygulamanız gerektiğinde, awkiyi bir seçenektir - ya bir astar (bazı insanlar bütün awk komut dosyalarını 'tek satırda' koyarlar, bu çok okunaklı olmasa da). kısa dış komut dosyası.

Gibi awk(senin kabuğu gibi) bir dil olması, bu o kadar verimli line-by-line işlem yapabileceği inanılmaz, ama bunun için özel olarak inşa edilmiş ve gerçekten çok hızlı.

Ve sonra Perl, metin dosyalarını işlemede çok iyi olan ve çok sayıda yararlı kütüphaneyle birlikte gelen çok sayıda başka betik dili var.

Ve son olarak, iyi eski C var, eğer maksimum hıza ve yüksek esnekliğe ihtiyacınız varsa (metin işleme biraz sıkıcı olsa da). Ancak, muhtemelen karşılaştığınız her farklı dosya işleme görevi için yeni bir C programı yazmak için zamanınızın çok kötü bir kullanımıdır. CSV dosyalarıyla çok çalışıyorum, bu yüzden C'de birçok farklı projede kullanabileceğim birkaç genel yardımcı program yazdım. Aslında bu, kabuk komut dosyalarımdan çağırabileceğim 'hızlı, hızlı Unix araçları' yelpazesini genişletiyor, bu yüzden çoğu projeye yalnızca her seferinde ısmarlama C kodu yazmak ve hata ayıklamaktan çok daha hızlı olan komut dosyaları yazarak başa çıkabiliyorum!

Bazı son ipuçları:

  • ana kabuk betiğinizi baştan başlatmayı unutmayın export LANG=C, yoksa birçok araç düz eski ASCII dosyalarınızı Unicode olarak görür, bu da onları daha yavaş yapar
  • Ayrıca , ortamdan bağımsız olarak tutarlı bir sipariş vermek export LC_ALL=Cistiyorsanız ayarlamayı düşünün sort!
  • sortVerilerinize ihtiyacınız varsa , bu muhtemelen her şeyden daha fazla zaman alacaktır (ve kaynaklar: CPU, bellek, disk), bu nedenle sortkomut sayısını ve sıraladıkları dosyaların boyutunu en aza indirmeye çalışın
  • mümkünse, tek bir boru hattı genellikle en verimlidir - sıralı, ara dosyalara sahip birden fazla boru hattını çalıştırmak, daha okunaklı ve hata ayıklanabilir, ancak programınızın harcadığı zamanı artırabilir

6
Pek çok basit aracın boru hatları (özellikle kafa, kuyruk, grep, sıralama, kesme, tr, sed, ... gibi belirtilenler), özellikle bu boru hattında zaten bir awk örneği varsa, gereksiz olarak kullanılır Bu basit araçların görevleri de. Dikkate alınacak bir diğer husus, boru hatlarında, boru hattının ön tarafındaki süreçlerden durum bilgilerini basit ve güvenilir bir şekilde arka taraftaki süreçlere aktaramayacağınızdır. Bu tür basit programlar için bir awk programı kullanıyorsanız, tek bir durum alanına sahipsiniz.
Janis

14

Evet ama...

Stéphane Chazelas doğru cevap dayanmaktadır özgü ikili gibi her metin çalışmasını delege kavramı grep, awk, sedve diğerleri.

As başına bir çok şey yapma yeteneğine sahip, bırakarak çatal (hatta bütün işini yaptığı için başka tercüman çalışan yerine) hızlı hale gelebilir.

Örnek için, bu yazıya bir göz atın:

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

ve

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

test et ve karşılaştır ...

Tabii ki

Kullanıcı girişi ve güvenliği dikkate alınmaz !

altında web uygulaması yazmayın !!

Ama sunucu yönetim görevlerine, bir çok yerine kullanılabilecek , builtins bash kullanarak çok etkili olabilir.

Benim anlamım:

Bin utils gibi yazma araçları sistem yönetimiyle aynı iş değildir.

Yani aynı insan değil!

Sistem yöneticilerinin bilmesi gereken yerlerde , tercih ettiği (ve en iyi bilinen) aracını kullanarak prototipshell yazabilirler .

Bu yeni yardımcı program (prototip) gerçekten faydalıysa, bazı insanlar daha uygun bir dil kullanarak özel araçlar geliştirebilirler.


1
İyi örnek. Yaklaşımınız kesinlikle lololux'dan daha etkilidir, ancak tensibai'nin cevabının (bu IMO'yu yapmanın doğru yolu, kabuk döngüleri kullanmadan) nasıl sizinkinden daha hızlı olduğuna dikkat edin. Ve seninki eğer çok hızlıdır yok kullanın bash. (sistemimde yaptığım testte ksh93 ile 3 kat daha hızlı). bashgenellikle en yavaş kabuktur. Hatta zshbu senaryoda iki kat daha hızlı. Ayrıca kote olmayan değişkenler ve kullanımı ile ilgili birkaç sorun var read. Demek aslında burada birçok noktamı gösteriyorsun
Stéphane Chazelas

@ StéphaneChazelas Katılıyorum, bash muhtemelen insanların bugün kullanabileceği en yavaş kabuktur , ama yine de en yaygın şekilde kullanılır.
F. Hauri,

@ StéphaneChazelas Ben gönderdiniz perl üzerinde versiyonunu Cevabıma
F. Hauri

1
@Tensibai, bulacaksın POSIXsh , awk , sed , grep, ed, ex, cut, sort, joinBash daha güvenilir ... hepsi veya Perl.
Wildcard

1
@Tensibai, U&L tarafından ilgilenen tüm sistemlerden, çoğu (Solaris, FreeBSD, HP / UX, AIX, çoğu Linux sistemi ...) bashvarsayılan olarak kurulmaz. bashÇoğunlukla yalnızca Apple MacOS ve GNU sistemleri (Sanırım bu dediğimiz herhalde üzerinde bulunan önemli dağılımları birçok sistemleri de (gibi isteğe bağlı bir paket olarak buna sahip olsa) zsh, tcl, python...)
Stéphane Chazelas
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.