3 poin oleh GN⁺ 2023-11-24 | 1 komentar | Bagikan ke WhatsApp
  • Pada sistem mirip Unix, bisa ada berkas eksekusi /bin/[ yang namanya berupa satu simbol; sintaks yang tampak seperti ekspresi kondisi shell sebenarnya berdiri di atas eksekusi perintah dan kode keluar
  • test mengevaluasi ekspresi dan mengembalikan 0 jika benar, 1 jika salah; jika dipanggil sebagai [, ia juga memeriksa apakah argumen terakhir adalah ]
  • Banyak shell juga menyediakan test dan [ sebagai perintah bawaan, sehingga pesan galat atau perilaku implementasi bawaan shell bisa berbeda dari /bin/test eksternal
  • Ekstensi Bash [[ bukan perintah eksternal melainkan sintaks bawaan, sehingga menerapkan aturan yang berbeda dari [, misalnya membandingkan long* sebagai string literal tanpa ekspansi glob seperti pada contoh
  • Untuk skrip portabel, [ adalah pilihan yang tepat; jika khusus Bash, lebih baik memakai [[ secara konsisten, tetapi pilihlah dengan memahami perbedaan aturan ekspansi di antara keduanya

Hakikat /bin/[ dan /bin/test

  • Pada sistem Unix, bisa ada berkas eksekusi /bin/[ yang namanya berupa satu simbol
    • Contoh perintah ls /bin/? menampilkan /bin/[
  • /bin/[ dan /bin/test bisa menunjuk ke biner yang sama
    • Dalam contoh, kedua jalur muncul sebagai berkas dengan inode dan ukuran yang sama
    • Namun, tidak harus berupa hard link di semua sistem
  • test adalah program yang mengevaluasi ekspresi di shell
    • Perbandingan string
    • Perbandingan angka
    • Pemeriksaan kondisi berkas
  • Jika hasil evaluasi benar, ia mengembalikan kode keluar 0; jika salah, 1

Mengapa [ berperilaku seperti perintah

  • test a = b sulit terlihat seperti ekspresi kondisi, tetapi jika logika yang sama ditulis sebagai [ a = b ], bentuknya menjadi lebih familier
  • [ tampak seperti sintaks tersendiri, tetapi sebenarnya adalah pemanggilan perintah
    • if [ a = b ]; then ... fi menjalankan perintah [ dan memeriksa kode keluarnya
    • Jika dipanggil sebagai [, program memeriksa apakah argumen terakhir adalah kurung siku penutup ]
  • Pernyataan if tidak menafsirkan ekspresi kondisi secara langsung, melainkan bercabang berdasarkan kode keluar perintah yang diberikan
    • test a = a; echo $? adalah 0
    • test a = b; echo $? adalah 1
    • [ a = a ]; echo $? adalah 0
    • [ a = b ]; echo $? adalah 1
  • Dalam konteks yang sama, true dan false juga dapat dipandang sebagai biner pembantu yang mengembalikan kode keluar

Perbedaan antara biner eksternal dan perintah bawaan shell

  • Karena test dan [ sering digunakan dalam skrip shell, kebanyakan shell juga mengimplementasikannya sebagai perintah bawaan
  • Untuk masukan yang sama, keluaran biner eksternal dan perintah bawaan shell bisa berbeda
    • /bin/test a b menghasilkan test: a: unexpected operator
    • test a b menghasilkan dash: 2: test: a: unexpected operator
  • Perbedaan semacam ini bisa terjadi bukan hanya pada test dan [, tetapi juga pada perintah yang tampak sederhana seperti echo
  • Implementasi bawaan berbeda-beda antar-shell, sehingga perilaku skrip dapat berubah tergantung shell yang menjalankannya

Aturan tersendiri yang diterapkan ekstensi Bash [[

  • [[ adalah ekstensi Bash dan dapat menggantikan penggunaan [
  • Perbedaan terbesar adalah [[ selalu merupakan sintaks bawaan
    • Berbeda dari [ yang bisa dijalankan sebagai biner eksternal, pada [[ Bash dapat mengubah aturan bahasa di dalam ekspresi
  • Dalam contoh glob, [ dan [[ berperilaku berbeda
    • Setelah touch long-name, [ long* = long-name ] && echo match mencetak match
    • Pada argumen untuk perintah [, aturan ekspansi shell biasa diterapkan, sehingga long* diekspansi menjadi long-name di direktori
    • [[ long* = long-name ]] && echo match tidak mencetak apa pun
    • [[ memperlakukan long* sebagai string literal dan membandingkannya apa adanya dengan long-name, sehingga gagal
  • Jika skrip khusus Bash, Anda juga dapat menggunakan fitur seperti pencocokan regex =~ dengan [[

Apa yang sebaiknya dipilih dalam skrip

  • Untuk skrip shell yang portabel, lebih tepat menggunakan [
  • test juga dapat digunakan, tetapi bukan pilihan yang umum
  • Jika skrip khusus Bash, lebih baik menggunakan [[ secara konsisten
  • Shell itu sendiri juga memiliki operator ekspresi seperti !, &&, dan ||
    • Operator ini bekerja berdasarkan status keluar perintah
    • grep ^hello$ ... && grep ^bye$ ... membuat kode keluar keseluruhan menjadi 0 jika kedua perintah berhasil
    • Jika grep pertama gagal, perintah setelah && tidak ikut berhasil, sehingga kode keluar keseluruhan menjadi 1
  • Karena itu, ekspresi test/[ dan operator logika shell dapat dikombinasikan dalam satu pernyataan kondisi
    • Contoh: [ a = b ] || grep -q ^hello$ /usr/share/dict/words
  • POSIX tidak mensyaratkan /bin/[ dan /bin/test harus berupa hard link
    • Di NetBSD, keduanya berupa hard link
    • macOS Catalina menyediakan salinan terpisah dari biner yang sama
    • Debian testing menyediakan dua biner yang berbeda
    • Spesifikasi POSIX tidak mensyaratkan kedua berkas tersebut berupa tautan

1 komentar

 
GN⁺ 2023-11-24
Pendapat Hacker News
  • Saya penulis aslinya. Terima kasih sudah membagikannya, dan saya senang ini sampai naik ke front page. Judulnya mungkin memang sebaiknya diberi (2020), dan karena "test" sebenarnya merujuk ke sebuah command, sebaiknya tidak ditulis dengan huruf kapital
    Saya juga menulis artikel terkait pada 2021; artikel itu membahas sampai operator [[ di bash, jadi sepertinya menarik untuk dibaca dalam konteks ini: https://jmmv.dev/2021/08/useless-use-of-gnu.html

    • Secara ketat, [[ bukan built-in command, melainkan pada dasarnya lebih dekat ke elemen sintaks. Mungkin secara internal ia memakai built-in command yang hampir tidak bisa diakses, tetapi hal menariknya adalah ]] juga merupakan reserved word meski tidak bisa muncul di posisi tempat reserved word bermakna
      Di beberapa shell non-bash, keyword function diperlukan untuk mendeklarasikan jenis fungsi tertentu. $(shell) di make bisa membuat perbedaan performa yang terukur saat membangun banyak target. Namun, jika tidak melakukan apa-apa, itu tetap merugikan, jadi biasanya memang lebih tepat memakai include untuk memicu regenerasi. GNU sepenuhnya wajar mengabaikan POSIX, karena POSIX tidak terlalu berguna untuk menyelesaikan sebagian besar masalah nyata
    • Banyak ekstensi GNU yang dibahas di https://jmmv.dev/2021/08/useless-use-of-gnu.html sangat berguna dalam penggunaan interaktif. Mencari di direktori saat ini tanpa menyebutkan . secara eksplisit juga berguna, dan menambahkan opsi di belakang command yang baru saja diketik benar-benar praktis. Saya selalu kesal melihat command yang tidak mendukung hal-hal seperti itu
      Dalam skrip, umumnya masuk akal untuk menyesuaikan diri dengan POSIX sh. Setidaknya kita harus tahu apakah sedang memakai sintaks khusus Bash
    • Artikel itu mengeluhkan bahwa memakai ekstensi GNU seperti --ignore-case dan set -o pipefail menurunkan portabilitas skrip. Itu sendiri benar
      Namun, artikel itu tidak menjelaskan mengapa pengguna Linux harus begitu peduli pada portabilitas. OpenBSD dan FreeBSD masih berjalan baik, tetapi jumlah penggunanya terlalu kecil sehingga rasanya bukan hal yang perlu terlalu dikhawatirkan. Demi fairness, bisa saja dikatakan bahwa sistem operasi seperti itu juga harus dipertimbangkan, tetapi di mana batasnya? Apakah hal obscure seperti vxWorks juga harus dipertimbangkan? Sisi BusyBox dan Alpine lebih menarik, tetapi perubahannya begitu besar sehingga hampir selalu tetap membutuhkan porting terpisah. Apakah ada alasan meyakinkan lain untuk peduli pada ekosistem non-GNU?
    • Bukankah sesuatu seperti if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fi hanyalah cara memakai shell yang biasa dan sehari-hari?
    • Sedikit di luar topik, tetapi sepertinya software blog-nya merusak judul. Misalnya ditampilkan sebagai make $(shell …) expansion, padahal seharusnya make $(shell ...) expansion
      Seperti yang tertulis dengan benar di isi artikel, itu tiga titik, bukan satu karakter elipsis, jadi mldr sendiri juga tidak tepat. Mungkin dua bug yang tidak saling terkait kebetulan sama-sama berpengaruh
  • Kalau poin terakhir didorong satu langkah lagi, blok if itu sendiri juga bisa dihilangkan. if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fi menjadi [ a = b ] && echo "Oops!" || echo "Expected; phew!"
    Saya tidak tahu seberapa sering kita perlu melakukan ini, tetapi kadang berguna saat mencetak output debug secara kondisional ke standard error, seperti [ "$debug" ] && echo "what's going on" >&2. Karena blok if sebenarnya memeriksa command biasa, hal seperti if grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi juga memungkinkan. Yang belum saya cari tahu adalah apakah harus menulis [ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ], atau memakai logical AND bawaan test, yaitu [ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Kalau performa bukan masalah, keduanya terlihat masuk akal karena alasan yang mirip

    • [ a = b ] && echo "Oops!" || echo "Expected; phew!" tidak boleh diterima sebagai aturan umum. Kemungkinan bash akan menafsirkan baris itu seperti ([ a = b ] && echo "Oops!") || echo "Expected; phew!"
      Jadi jika rangkaian command setelah && gagal, kode setelah || tetap akan dijalankan. Misalnya, jika >/dev/full echo "strings match" gagal karena error tulis, maka "strings don't match" akan tercetak meskipun string-nya sama. Ini berbeda dari makna blok if
    • Sebaiknya hindari singkatan semacam itu. Jika Anda memakai set -e yang memang sebaiknya dipakai, if [ a = b ]; then echo "Oops!"; fi akan berperilaku seperti yang diharapkan, tetapi [ a = b ] && echo "Oops!" akan keluar sebagai error ketika ekspresi a tidak sama dengan b
    • Menurut POSIX, primary biner -a, -o serta operator (, ) ditandai sebagai akan dihentikan penggunaannya. Untuk detailnya, lihat "Application Usage" di https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t...
    • Baik memakai -a maupun dua test dengan &&, kalau bisa memakai evaluasi aritmetika bash, tidak perlu menjalankan expr: [ $((1+1)) -eq 2 ]
  • Sejak beberapa tahun lalu saya mulai tidak memakai [. test menegaskan bahwa ini bukan sintaks, melainkan hanya sebuah command seperti command lainnya. Dan man test jauh lebih nyaman daripada mengubek-ubek man bash

    • Itu kurang nyambung. GNU Coreutils punya halaman manual man [ selain man test
      Di Bash ada help test sebagai cheatsheet cepat. Command [ sudah sangat tua, dan sudah ada di Version 7 Unix pada 1979
  • Setuju. [ dan [[ yang khusus bash memang menimbulkan banyak kebingungan tentang apa yang sebenarnya terjadi, jadi sulit untuk merasa yakin
    Namun fakta bahwa [[ dijamin sebagai perintah bawaan jelas punya tujuan pada masa ketika performa skrip shell masih berarti, dan itu juga belum terlalu lama

    • Setelah membaca tulisan ini, sepertinya saya akan memakai test saja. Saya tidak sering menulis skrip bash, jadi selalu tersandung pada pernyataan if, terutama aturan spasi. Setelah melihat alasannya, semuanya jadi terasa sangat masuk akal, dan memakai test lebih jelas menunjukkan bahwa kita hanya meneruskan argumen
  • Jebakan terbesar pada [ dan test adalah perilaku dengan satu argumen. Misalnya, untuk memeriksa bahwa sebuah variabel tidak kosong, kita bisa menulis [ -n $FOO ]
    Namun jika FOO tidak disetel, itu akan diekspansi bukan menjadi string kosong, melainkan tidak ada apa-apa, sehingga menjadi sama seperti [ -n ]. POSIX mensyaratkan bahwa dalam bentuk [ dengan satu argumen, perintah harus berhasil jika argumen itu, dalam hal ini "-n", tidak kosong. Jadi ia keliru melaporkan bahwa $FOO tidak kosong. Variabel wajib diapit tanda kutip

    • Kalimat terakhir seharusnya dipindahkan ke paling depan. Apit variabel dengan tanda kutip. Jebakannya bukan ada pada spesifikasi perintah bawaan test itu sendiri, melainkan pada shell itu sendiri
      Perilaku yang disebutkan itu masuk akal. [ "$FOO" ] adalah bentuk yang selalu memeriksa apakah isinya tidak kosong, apa pun isinya, bahkan jika itu "-n"
    • Jalankan ShellCheck pada skrip
    • Jika $FOO berisi spasi, ia akan diekspansi menjadi beberapa argumen. Pokoknya selalu apit variabel dengan tanda kutip
    • [ x"$FOO" != x"" ]
    • Untuk kasus ini, saya akan memakai [ -n "${FOO?}" ] agar skrip langsung berhenti ketika $FOO null atau tidak disetel
  • chubot menulis beberapa dokumen menarik yang menggali bagian-bagian lebih subtil dari test/[/[[. Tulisan-tulisan lain di blog itu juga menjelaskan keanehan shell dengan cukup menarik
    ¹ https://www.oilshell.org/blog/2017/08/31.html
    ² https://www.oilshell.org/blog/2016/11/18.html

  • Saya sama sekali tidak tahu bahwa [ adalah sebuah program, dan agak lucu bahwa ia memeriksa apakah argumen terakhir adalah kurung siku penutup
    Tapi itu menjelaskan kenapa spasi diperlukan di kedua sisi kurung siku

  • [[ khusus bash. Kalau Anda tahu hanya akan memakai bash, silakan gunakan. Tulisan itu membahas detailnya dengan baik

    • zsh juga punya :)
      https://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
    • Selalu pakai [[ saja
      zsh dan ksh juga punya, dan saya hampir yakin sebenarnya itu bermula dari ksh pada 1988 atau sebelumnya
    • Jadi test dan [ ditentukan dalam POSIX dan biasanya memang ada sebagai binary sungguhan. Namun perintah bawaan shell bisa saja menutupinya
      Sebaliknya, [[ tidak ditentukan dalam POSIX, dan biasanya hanya ada sebagai bawaan shell
    • Kecuali Anda secara eksplisit memakai shell yang benar-benar berbeda seperti Fish, saya tidak mengerti kenapa tidak memakai Bash
      Menargetkan shell penyebut bersama terkecil terasa seperti kekhawatiran kuno sepenuhnya
  • Saya tidak begitu paham kenapa pernyataan if terakhir membingungkan. Kalau alasannya karena saat pertama belajar skrip shell orang biasanya menganggap [ adalah bagian dari bahasa skrip bash, bukan sekadar program lain, sekarang saya paham. Kalau bukan itu, saya ingin ada penjelasan kenapa itu mengejutkan

    • Meski tidak tahu bahwa [ adalah binary, saya tetap tidak begitu mengerti kenapa itu membingungkan. Itu terlihat seperti bash yang sangat biasa
  • Saya punya selera yang kuat soal shell, dan itu tidak terlalu cocok dengan kebanyakan dunia
    Menurut saya [ tidak boleh dipakai sama sekali, dan hanya test yang seharusnya dipakai. [ membuat orang salah mengira mekanismenya adalah bagian dari sintaks bahasa, padahal sebenarnya itu hanya satu lagi "program". Di sini "program" mencakup perintah bawaan dan fungsi. if/||/&& melihat status keluar, dan setelah diekspansi, sebuah program tidak bisa melihat status keluar apa pun selain melihat variabel ajaib $? yang hanyalah string. case melihat string, tetapi tidak bekerja berdasarkan status keluar dan juga tidak menetapkan status keluar sebagai bagian dari perilaku case ... esac. "Program"-lah yang menetapkan status keluar. Selain itu, [/test seharusnya hanya dipakai untuk mengevaluasi struktur filesystem, seperti test -f /dev/null. Menurut saya evaluasi string seharusnya memakai case. Tentu saja kebanyakan skrip membuat saya gatal, dan skrip yang saya tulis dianggap aneh oleh orang lain

    • Leluconnya berbalik ke saya. Itu memang inti tulisannya
      Saat memakai shell, saya lebih suka menaruh program setelah if di baris terpisah lalu lanjut ke then. Ini untuk menekankan bahwa yang dilihat adalah "status keluar dari perintah terakhir sebelum then". Misalnya dalam struktur seperti 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, if pada akhirnya hanya melihat status keluar dari echo, sehingga klausa then selalu dijalankan
    • Bertemu sesama pejalan di jalan yang sama. Delapan tahun skrip berisi test menjadi bukti bahwa saya setuju. Ini jalan yang sepi... Saya menyalahkan Google shell style guide
      Kebiasaan memilih test muncul saat menulis skrip yang harus berjalan di sh maupun bash, tetapi saya terus memakainya karena secara semantik lebih masuk akal daripada memperlakukan karakter [ seperti perintah. Fakta bahwa ] bukan binary terpisah melainkan argumen untuk [ juga aneh. Saya paham alasan teknisnya, tapi tetap terasa seperti hack
    • Dari membaca komentar di sini, sepertinya ada puluhan orang yang entah bagaimana hanya memakai test. Puluhan!
  • Saya diajari untuk memakai [[ hanya ketika ingin melakukan pencocokan regex. Contoh: if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fi
    Selain itu saya cukup memakai "test" atau "[". Saat ini sudah 85.000 baris bash. Bukan berarti bash itu hebat, tetapi sejauh ini masih memenuhi kebutuhan saya untuk banyak hal

    • Jika ingin melakukan pattern matching yang relatif sederhana dengan cara yang kompatibel POSIX, expr bisa mencocokkan regular expression dasar, dan juga bisa mengembalikan capture group

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

  • Saya sempat kena cukup lama karena fakta bahwa regex ditulis apa adanya dan tidak diapit tanda kutip. Karena saya tipe orang yang secara religius selalu memberi tanda kutip pada apa pun, butuh waktu untuk mengetahui kenapa regex yang bodohnya sederhana itu tidak cocok.
  • Contoh itu tidak begitu berarti. Cukup pakai [ "$foo" = bar ] && echo Yes
    Untuk pencocokan substring, [ dan glob * biasanya sudah cukup. Misalnya seperti [ "$bar" = extra* ] && echo '$bar began with extra'. Dialek regex Bash itu primitif, jadi hampir tidak sepadan untuk dipakai dengan susah payah. Untuk hal yang rumit, sebaiknya gunakan alat lain seperti grep, awk, atau perl. Kalau terobsesi mengerjakan semuanya dengan bash sampai tugas kompleks yang membutuhkan reusabilitas, modularitas, dan tipe bawaan yang lebih tinggi, hasilnya akan cepat sekali makin tidak sepadan.