3 puan yazan GN⁺ 2023-11-24 | 1 yorum | WhatsApp'ta paylaş
  • Unix benzeri sistemlerde adı tek karakterlik bir sembol olan /bin/[ çalıştırılabilir dosyası bulunabilir; kabuk koşulu gibi görünen söz dizimi de aslında komut çalıştırma ve çıkış kodları üzerine kuruludur
  • test, ifadeyi değerlendirir; doğruysa 0, yanlışsa 1 döndürür ve [ olarak çağrıldığında son argümanın ] olup olmadığını da kontrol eder
  • Birçok kabuk test ve [ komutlarını yerleşik komut olarak da sunduğundan, harici /bin/test ile kabuğun yerleşik uygulamasının hata mesajları veya davranışları farklı olabilir
  • Bash uzantısı olan [[, harici bir komut değil yerleşik bir söz dizimidir; bu yüzden örnekteki gibi long* değerini glob genişletmesine sokmadan literal dize olarak karşılaştırmak gibi [den farklı kurallar uygular
  • Taşınabilir betikler için [ uygundur; yalnızca Bash’e özel betiklerde ise [[yi tutarlı biçimde kullanmak daha iyidir, ancak iki yöntemin genişletme kuralları arasındaki farkı bilerek seçim yapmak gerekir

/bin/[ ve /bin/test nedir?

  • Unix sistemlerinde adı tek karakterlik bir sembol olan /bin/[ çalıştırılabilir dosyası bulunabilir
    • Örnek komut ls /bin/?, /bin/[ dosyasını gösterir
  • /bin/[ ve /bin/test aynı ikili dosyayı işaret ediyor olabilir
    • Örnekte iki yol, aynı inode’a ve boyuta sahip dosyalar olarak görünür
    • Ancak her sistemde hard link olmaları gerekmez
  • test, kabukta ifadeleri değerlendiren bir programdır
    • Dize karşılaştırma
    • Sayı karşılaştırma
    • Dosya koşullarını kontrol etme
  • Değerlendirme sonucu doğruysa çıkış kodu olarak 0, yanlışsa 1 döndürür

[ neden komut gibi davranır?

  • test a = b bir koşul ifadesi gibi görünmekte zorlanır; ancak aynı mantık [ a = b ] şeklinde yazıldığında daha tanıdık bir biçim alır
  • [ ayrı bir söz dizimi gibi görünse de aslında bir komut çağrısıdır
    • if [ a = b ]; then ... fi, [ komutunu çalıştırır ve çıkış kodunu kontrol eder
    • [ olarak çağrıldığında program, son argümanın kapatan köşeli parantez ] olup olmadığını denetler
  • if ifadesi koşulu doğrudan yorumlamak yerine, verilen komutun çıkış koduna göre dallanır
    • test a = a; echo $? sonucu 0
    • test a = b; echo $? sonucu 1
    • [ a = a ]; echo $? sonucu 0
    • [ a = b ]; echo $? sonucu 1
  • Aynı bağlamda true ve false da çıkış kodu döndüren yardımcı ikili dosyalar olarak görülebilir

Harici ikili dosya ile kabuk yerleşik komutu arasındaki fark

  • test ve [ kabuk betiklerinde sık kullanıldığı için çoğu kabuk bunları yerleşik komut olarak da uygular
  • Aynı girdi için bile harici ikili dosya ile kabuk yerleşik komutunun çıktısı farklı olabilir
    • /bin/test a b çıktısı test: a: unexpected operator
    • test a b çıktısı dash: 2: test: a: unexpected operator
  • Bu tür farklar yalnızca test ve [ için değil, echo gibi basit görünen komutlarda da ortaya çıkabilir
  • Yerleşik uygulama kabuktan kabuğa değiştiğinden, betiğin davranışı onu çalıştıran kabuğa göre farklılaşabilir

Bash uzantısı [[nin uyguladığı ayrı kurallar

  • [[ bir Bash uzantısıdır ve [ kullanımının yerine geçebilir
  • En büyük fark, [[nin her zaman yerleşik bir söz dizimi olmasıdır
    • Harici ikili dosya olarak çalıştırılabilen [den farklı olarak, [[ içinde Bash ifade içindeki dil kurallarını değiştirebilir
  • Glob örneğinde [ ve [[ birbirinden farklı davranır
    • touch long-name sonrasında [ long* = long-name ] && echo match, match yazdırır
    • [ komutunun argümanlarına normal kabuk genişletme kuralları uygulanır ve long*, dizindeki long-name olarak genişletilir
    • [[ long* = long-name ]] && echo match çıktı üretmez
    • [[, long* değerini literal dize olarak ele alıp long-name ile aynen karşılaştırdığı için başarısız olur
  • Bash’e özel betiklerde [[ ile düzenli ifade eşleştirme =~ gibi özellikler de kullanılabilir

Betiklerde hangisi seçilmeli?

  • Taşınabilir kabuk betikleri için [ kullanmak daha doğrudur
  • test de kullanılabilir, ancak yaygın tercih değildir
  • Betik yalnızca Bash’e özelse [[yi tutarlı biçimde kullanmak daha iyidir
  • Kabuğun kendisinde de !, &&, || gibi ifade operatörleri vardır
    • Bu operatörler komutların çıkış durumuna göre çalışır
    • grep ^hello$ ... && grep ^bye$ ..., iki komut da başarılı olursa toplam çıkış kodunun 0 olmasını sağlar
    • İlk grep başarısız olursa && sonrasındaki komut da başarılı olmayacağından toplam çıkış kodu 1 olur
  • Bu nedenle test/[ ifadeleri ile kabuğun mantıksal operatörleri tek bir koşul ifadesi içinde birleştirilebilir
    • Örnek: [ a = b ] || grep -q ^hello$ /usr/share/dict/words
  • POSIX, /bin/[ ve /bin/test dosyalarının hard link olmasını şart koşmaz
    • NetBSD’de hard link idi
    • macOS Catalina, aynı ikili dosyanın ayrı kopyalarını sağlar
    • Debian testing, birbirinden farklı ikili dosyalar sağlar
    • POSIX belirtimi, iki dosyanın link olmasını şart koşmaz

1 yorum

 
GN⁺ 2023-11-24
Hacker News görüşleri
  • Orijinal yazının yazarı benim. Paylaştığınız için teşekkürler; ön sayfaya kadar çıkmasına sevindim. Başlıkta muhtemelen (2020) yer almalı ve "test" gerçek bir komutu ifade ettiği için büyük harfle yazılmaması daha doğru olur.
    2021'de yazdığım ilgili bir yazı daha var; onda bash'in [[ operatörüne kadar değiniyorum, bu bağlamda ilginç gelebilir: https://jmmv.dev/2021/08/useless-use-of-gnu.html

    • [[ teknik olarak yerleşik bir komut değil, özünde bir sözdizimi öğesine daha yakın. Muhtemelen içeride neredeyse erişilemeyen bir yerleşik komut kullanıyordur, ama ilginç olan şu ki ]] de anlamlı bir konuma gelemeyen ayrılmış bir sözcük olmasına rağmen yine de ayrılmış sözcüktür.
      Bash dışı bazı kabuklarda belirli türde fonksiyon bildirmek için function anahtar sözcüğü gerekir. make içindeki $(shell), çok sayıda hedef derlenirken ölçülebilir bir performans farkı yaratabilir. Yine de hiçbir iş yapmıyorsa zarardır; bu yüzden genelde yeniden üretimi tetiklemek istiyorsanız include kullanmak daha doğrudur. GNU'nun POSIX'i görmezden gelmesi tamamen makul, çünkü POSIX çoğu gerçek problemi çözmekte pek faydalı değil
    • https://jmmv.dev/2021/08/useless-use-of-gnu.html yazısında ele alınan GNU genişletmelerinin önemli bir kısmı etkileşimli kullanımda çok faydalı. Geçerli dizini açık bir . olmadan aramak da faydalı, az önce yazdığınız komuta sonradan seçenek ekleyebilmek de gerçekten çok pratik. Bunu desteklemeyen komutları görünce hep sinir oluyorum.
      Betiklerde POSIX sh'ye uymak genelde mantıklı. En azından Bash'e özgü sözdizimi kullandığınızın farkında olmalısınız
    • O yazı, --ignore-case, set -o pipefail gibi GNU genişletmelerini kullanmanın betiğin taşınabilirliğini azalttığından şikâyet ediyor. Bu kendi başına doğru.
      Ama Linux kullanıcılarının neden taşınabilirliği bu kadar önemsemesi gerektiğini açıklamıyor. OpenBSD ve FreeBSD hayatta ve iyi durumda ama kullanıcı sayıları o kadar az ki özellikle endişe edilmesi gereken bir kitle gibi görünmüyor. Hakkaniyet adına böyle işletim sistemlerini de düşünmek gerekir denebilir, ama bu çizgi nerede biter? vxWorks gibi obscure şeyleri de mi hesaba katmalıyız? BusyBox ve Alpine tarafı daha ilginç, ama değişiklikler o kadar büyük ki zaten neredeyse her zaman ayrı bir portlama gerekiyor. GNU dışı ekosistemleri önemsemek için başka ikna edici bir neden var mı?
    • if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fi gibi bir şey, sıradan günlük kabuk kullanımı değil mi?
    • Yan bir not olarak, blog yazılımı başlığı bozmuş gibi görünüyor. Örneğin make $(shell …) expansion olarak gösteriliyor, ama aslında make $(shell ...) expansion olması gerekiyordu.
      Metnin gövdesinde doğru yazıldığı gibi bu bir üç nokta değil, tek bir ellipsis de değil; dolayısıyla mldr de doğru değil. Muhtemelen birbiriyle ilgisiz iki ayrı hata aynı anda etkili olmuş
  • Son noktayı bir adım daha ileri götürürsek if bloğunun kendisini de kaldırabilirsiniz. if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fi, [ a = b ] && echo "Oops!" || echo "Expected; phew!" hâline gelir.
    Bunu ne kadar sık yapmak gerekir bilmiyorum ama [ "$debug" ] && echo "what's going on" >&2 gibi standart hataya koşullu debug çıktısı vermek için bazen faydalı oluyor. if bloğunun sıradan bir komutu sınaması sayesinde if grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi gibi şeyler de mümkün. Henüz incelemediğim konu, bunu [ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ] şeklinde mi yazmak gerektiği, yoksa test komutunun yerleşik mantıksal VE'si olan [ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ] biçiminin mi tercih edilmesi gerektiği. Performans sorun değilse iki yaklaşım da benzer nedenlerle makul görünüyor

    • [ a = b ] && echo "Oops!" || echo "Expected; phew!" ifadesini genel bir kural gibi benimsememek gerekir. Bash bu satırı muhtemelen ([ a = b ] && echo "Oops!") || echo "Expected; phew!" şeklinde yorumlar.
      Bu yüzden && sonrasındaki komut dizisi başarısız olursa || sonrasındaki kod her durumda çalışır. Örneğin >/dev/full echo "strings match" yazma hatasıyla başarısız olursa, dizgeler eşit olduğu hâlde "strings don't match" yazdırılır. Bu, if bloğunun anlamıyla aynı değildir
    • Böyle kısaltmalardan kaçınmak daha iyi. Kullanmanız gereken set -e ayarını kullanıyorsanız, if [ a = b ]; then echo "Oops!"; fi beklendiği gibi çalışır, ama [ a = b ] && echo "Oops!" ifadesi a ifadesi b'ye eşit olmadığında hatayla sonlanır
    • POSIX'e göre -a, -o ikili temel ifadeleri ile (, ) operatörleri kullanımdan kaldırılmak üzere işaretlenmiştir. Ayrıntılar için https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t... içindeki "Application Usage" bölümüne bakabilirsiniz
    • -a kullansanız da iki test ve && kullansanız da, bash'in aritmetik değerlendirmesini kullanabiliyorsanız expr çalıştırmak için dışarı çıkmanıza gerek yok: [ $((1+1)) -eq 2 ]
  • Birkaç yıldır [ kullanmayı bıraktım. test, bunun sözdizimi değil de diğerleri gibi bir komut olduğunu daha net vurguluyor. Ayrıca man bash içinde arama yapmaktansa man test çok daha rahat

    • Bu pek tutarlı değil. GNU Coreutils'te yalnızca man test değil, man [ kılavuz sayfası da var.
      Bash'te hızlı bir kopya kâğıdı olarak help test mevcut. [ komutu çok eskidir; 1979'daki Version 7 Unix'te zaten bulunuyordu
  • Katılıyorum. [ ve bash’e özgü [[, gerçekte neler olduğuna dair çok fazla kafa karışıklığı yaratıyor; bu yüzden emin olmak zordu
    Yine de [[ için yerleşik komut olmasının garanti edilmesi, kabuk betiği performansının anlamlı olduğu dönemlerde açıkça bir amaca hizmet ediyordu; hem de o kadar eski bir konu değil

    • Bu yazıyı okuduktan sonra sanırım bundan sonra sadece test kullanacağım. Sık sık bash betiği yazmadığım için if ifadelerinde, özellikle de boşluk kurallarında, hep takılıyorum. Sebebini görünce çok bariz geldi ve test kullanınca bunun sadece argüman geçirmekten ibaret olduğu daha net görünüyor
  • [ ve test ile ilgili en büyük tuzak, tek argümanlı davranış. Örneğin bir değişkenin boş olmadığını kontrol etmek için [ -n $FOO ] yazabilirsiniz
    Ama FOO ayarlanmamışsa boş dizeye değil, hiçbir şeye genişler; yani [ -n ] ile aynı hale gelir. POSIX, [ komutunun tek argümanlı biçiminde, o argümanın — burada "-n" — boş olmaması halinde başarılı olmasını şart koşar. Bu yüzden $FOO boş değilmiş gibi yanlış sonuç verir. Değişkenleri mutlaka tırnak içine almalısınız

    • Son cümle en başa taşınmalı. Değişkenleri tırnak içine alın. Tuzak test yerleşik komutunun tanımında değil, doğrudan kabuğun kendisinde
      Söz konusu davranış mantıklı. Çünkü [ "$FOO" ], içeriği ne olursa olsun, hatta "-n" olsa bile, her zaman boş olup olmadığını sınayan bir biçim
    • Betiklerde ShellCheck çalıştırın yeter
    • $FOO içinde boşluk varsa birden çok argümana genişler. Kısacası her zaman değişkenleri tırnak içine alın
    • [ x"$FOO" != x"" ]
    • Bu durumda [ -n "${FOO?}" ] kullanırdım; böylece $FOO null ya da tanımsızsa betik hemen durur
  • chubot, test/[/[[ ile ilgili daha incelikli noktaları kurcalayan ilginç bir belge yazmış. O blogdaki diğer yazılar da kabuğun tuhaf yanlarını epey ilginç biçimde açıklıyor
    ¹ https://www.oilshell.org/blog/2017/08/31.html
    ² https://www.oilshell.org/blog/2016/11/18.html

  • [’ın bir program olduğunu hiç bilmiyordum ve son argümanın kapanış köşeli parantezi olup olmadığını kontrol etmesi biraz komik
    Yine de bu, köşeli parantezlerin iki yanında neden boşluk gerektiğini açıklıyor

  • [[ bash’e özgü. Yalnızca bash kullanacağınızı biliyorsanız kullanın. Ayrıntıları yazı iyi ele alıyor

    • zsh de var :)
      https://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
    • Hep [[ kullanabilirsiniz
      zsh ve ksh’de de var; hatta 1988’de ya da daha önce ksh ile başladığından neredeyse eminim
    • Yani test ve [ POSIX’te tanımlıdır ve genelde gerçek ikililer olarak bulunur. Ama kabuk yerleşik komutları bunları gölgeleyebilir
      Buna karşılık [[, POSIX’te tanımlı değildir ve genelde yalnızca bir kabuk yerleşiği olarak bulunur
    • Fish gibi açıkça tamamen farklı bir kabuk kullanmıyorsanız neden Bash kullanmadığınızı bilmiyorum
      Sadece en küçük ortak kabuğu hedeflemek tamamen antik bir kaygı gibi geliyor
  • Son if ifadesinin neden kafa karıştırıcı olduğunu pek anlayamadım. Kabuk betiğini ilk öğrenirken çoğu kişinin [’ı sadece başka bir program değil, bash betik dilinin bir parçası sandığını kastediyorsa şimdi anlıyorum. Yoksa neden şaşırtıcı olduğunu açıklaması iyi olurdu

    • [’ın bir ikili olduğunu bilmeseniz bile neden kafa karıştırıcı olduğunu anlamıyorum. Gayet sıradan bir bash gibi görünüyor
  • Kabuk konusunda güçlü tercihleri olan biriyim ve bu tercihler dünyanın geri kalanıyla pek uyuşmuyor
    [ asla kullanılmamalı, sadece test kullanılmalı diye düşünüyorum. [ onun mekanizmasını dil sözdiziminin bir parçasıymış gibi düşündürüyor, ama gerçekte sadece bir başka "program". Buradaki "program"a yerleşik komutlar ve fonksiyonlar da dahil. if/||/&& çıkış durumuna bakar; programlar ise genişletmeden sonra yalnızca dize olan sihirli $? değişkenine bakmak dışında başka şeylerin çıkış durumunu göremez. case dizelere bakar, ama çıkış durumuna göre çalışmaz ve case ... esac işleyişinin bir parçası olarak çıkış durumu da ayarlamaz. Çıkış durumunu ayarlayan şey "program"dır. Ayrıca [/test yalnızca dosya sistemi yapısını değerlendirmek için kullanılmalı; örneğin test -f /dev/null gibi. Dize değerlendirmelerinde ise case kullanılmalı diye düşünüyorum. Doğal olarak çoğu betiği görünce içim kaşınıyor, benim yazdıklarımı da başkaları garip buluyor

    • Şaka bana dönmüş oldu. Yazının asıl anlatmak istediği tam da buydu
      Kabuk kullanırken iften sonra programı ayrı satıra koyup sonra thene geçmeyi tercih ediyorum. Çünkü bu, ifin "thenden önceki son komutun çıkış durumuna baktığını" vurguluyor. Mesela if; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fi gibi bir yapıda if sonuçta sadece echonun çıkış durumuna baktığı için then bölümü her zaman çalışır
    • Aynı yolda yürüyen birini görmek güzel. 8 yıllık test içeren betiklerim aynı fikirde olduğumun kanıtı. Yalnız bir yol... Bunun suçlusu olarak Google kabuk stil kılavuzunu görüyorum
      Hem sh hem bash üzerinde çalışması gereken betikler yazarken test tercih etme alışkanlığı edindim, ama köşeli parantez karakteri [’ı komutmuş gibi ele almaktan daha anlamlı geldiği için bunu sürdürdüm. ]’nin ayrı bir ikili değil de [’ın argümanı olması da tuhaf. Teknik nedenini anlıyorum ama hack gibi hissettiriyor
    • Buradaki yorumları okuyunca bir şekilde sadece test kullanan onlarca kişi var gibi görünüyor. Onlarca kişi!
  • Bunu, yalnızca regex eşleştirmesi yapmak istediğinizde [[ kullanmak şeklinde öğrenmiştim. Örneğin: if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fi
    Onun dışında sadece "test" ya da "[" kullanıyorum. 85.000 satırdır bash yazıyorum. Bash’in harika olduğunu söylemiyorum ama hâlâ birçok işte ihtiyacımı karşılıyor

    • Görece basit desen eşleştirmeyi POSIX uyumlu biçimde yapmak istiyorsanız expr, temel düzenli ifadeleri eşleştirebilir ve yakalama gruplarını da döndürebilir

1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...

  • Normal ifadeyi olduğu gibi kullanıp tırnak içine almamaları yüzünden bir süre bununla uğraşmıştım. Her şeye refleks olarak tırnak koyan biri olduğum için, aptalca basit bir düzenli ifadenin neden eşleşmediğini anlamam zaman aldı.
  • O örneğin pek bir anlamı yok. [ "$foo" = bar ] && echo Yes olması yeterli.
    Alt dize eşleştirmesi için çoğu durumda [ ve * glob kullanmak yeterlidir. Mesela [ "$bar" = extra* ] && echo '$bar began with extra' gibi. Bash'in regex lehçesi ilkel olduğu için özellikle kullanmaya pek değmez. Karmaşık işler için grep, awk, perl gibi başka araçları kullanmak daha doğru. Daha yüksek yeniden kullanılabilirlik, modülerlik ve yerleşik tipler gerektiren karmaşık işlerin hepsini bash ile yapmaya inat edersen, getirisi hızla azalır.