bash, POSIX, dan zsh

Tingkat portabilitas skrip antara POSIX sh, bash, dan zsh beserta konsekuensi pilihan shebang.

Daftar isi

Skrip shell tidak selalu dijalankan oleh program yang sama pada setiap sistem. Topik ini membedakan tiga tingkat bahasa shell, yaitu POSIX sh, bash, dan zsh, menjelaskan cara shebang memilih penafsir, serta menunjukkan bashism yang gagal di penafsir POSIX. Setelah membaca topik ini, pilihan shebang untuk sebuah skrip dapat ditentukan secara sadar menurut tingkat portabilitas yang dituju.

Tiga lapis bahasa shell#

POSIX (Portable Operating System Interface) adalah keluarga standar untuk sistem operasi bertipe Unix, dan salah satu bagiannya, POSIX.1-2017 Shell Command Language, menetapkan bahasa shell minimum yang wajib dipahami setiap shell standar. dash (Debian Almquist shell) adalah implementasi yang sengaja hanya menargetkan lapisan POSIX, sehingga ukurannya kecil dan eksekusinya cepat. bash (Bourne Again shell) menambahkan array, uji kondisi [[ ]], dan banyak fitur lain di atas lapisan POSIX. zsh (Z shell) juga menambahkan fitur, tetapi arah perluasannya berbeda, misalnya penomoran array yang dimulai dari 1.

Tabel berikut merangkum tingkat portabilitas beberapa fitur pada ketiga lapis tersebut.

PerilakuPOSIX shbashzsh
Uji kondisi [ ]tersediatersediatersedia
Uji kondisi [[ ]]tidak adatersediatersedia
array berindekstidak adatersedia, indeks mulai 0tersedia, indeks mulai 1
substitusi proses <( )tidak adatersediatersedia
pemisahan kata pada perluasan variabel tanpa kutipdilakukandilakukantidak dilakukan secara default
substitusi perintah $( )tersediatersediatersedia

Baris pemisahan kata pada tabel menunjukkan bahwa zsh bukan pengganti bash yang sepenuhnya kompatibel, sehingga skrip yang diuji di bash tetap perlu diuji lagi sebelum dinyatakan kompatibel dengan zsh. Batasan serupa juga berlaku terhadap POSIX. Subset POSIX perlu dipisahkan secara eksplisit dari bashism sesuai profil POSIX.1-2017 agar batas portabilitas sebuah skrip dapat dinyatakan dengan pasti, bukan hanya diasumsikan.

Shebang menentukan penafsir#

Shebang adalah baris pertama skrip yang diawali #! dan menyebut program penafsir yang menjalankan skrip tersebut. Ketika skrip dieksekusi langsung sebagai ./skrip.sh, kernel (inti sistem operasi) membaca shebang dan menyerahkan isi skrip ke program yang disebut di baris itu. Shebang #!/bin/sh menyerahkan skrip ke program /bin/sh, sedangkan #!/usr/bin/env bash mencari program bash melalui PATH sehingga tetap bekerja pada sistem yang menaruh bash di lokasi tidak standar, misalnya FreeBSD atau instalasi Homebrew di macOS.

Program di balik /bin/sh berbeda antar sistem. Identifikasi berikut dilakukan di sandbox ini:

bash
# mengidentifikasi program yang sesungguhnya berada di balik /bin/sh
readlink -f /bin/sh
text Keluaran
/usr/bin/dash

Pada sandbox ini /bin/sh menunjuk dash, sehingga skrip dengan shebang #!/bin/sh dijalankan dash dan hanya boleh memakai subset POSIX. Pada distribusi lain /bin/sh bisa menunjuk bash, dan pada macOS /bin/bash berupa bash versi 3.2 yang tidak memiliki banyak fitur bash modern. Keadaan ini membuat shebang menjadi keputusan portabilitas yang sesungguhnya, bukan formalitas penulisan.

Bashism dan pesan error di dash#

Bashism adalah fitur bash yang berada di luar subset POSIX, misalnya array dan uji kondisi [[ ]]. Skrip berikut memakai keduanya, sehingga skrip hanya berfungsi ketika dijalankan oleh bash.

bash contoh-array.sh
#!/usr/bin/env bash
# contoh bashism: array tidak dikenal POSIX sh
warna=("merah" "hijau" "biru")
echo "warna pertama: ${warna[0]}"
echo "jumlah warna: ${#warna[@]}"
text Keluaran
warna pertama: merah
jumlah warna: 3

Menjalankan skrip yang sama dengan dash menghasilkan error sintaks:

bash
# menjalankan skrip bashism dengan penafsir POSIX
dash contoh-array.sh
echo "kode keluar: $?"
text Keluaran
contoh-array.sh: 3: Syntax error: "(" unexpected
kode keluar: 2

Pesan error dash menunjuk baris 3, yaitu baris pertama yang memuat tanda kurung array, dan skrip berhenti dengan kode keluar 2 sebelum satu perintah pun dieksekusi. Perilaku serupa terlihat pada [[ ]]:

bash
# [[ ]] tersedia di bash
[[ "abc" == a* ]] && echo "pola cocok"
# [[ ]] tidak dikenal dash
dash -c '[[ "abc" == a* ]] && echo "pola cocok"'
echo "kode keluar: $?"
text Keluaran
pola cocok
dash: 1: [[: not found
kode keluar: 127

Kode keluar 127 berarti dash mencari perintah bernama [[ dan tidak menemukannya, karena dash tidak mengenal konstruksi tersebut sama sekali. Dua contoh di atas memperlihatkan pola kegagalan bashism yang khas, yaitu error muncul pada penafsir POSIX sementara bash menerima skrip tanpa keluhan.

Subset POSIX untuk skrip portabel#

Skrip portabel ditulis dengan membatasi konstruksi yang dipakai pada subset POSIX. Berkas log contoh dibuat terlebih dahulu:

bash
# membuat berkas log contoh
printf '%s\n' "INFO suhu 24.5" "ERROR sensor 3" "INFO suhu 25.1" "ERROR sensor 4" > sensor.log

Skrip berikut membuat laporan ringkas dari log tersebut dan hanya memakai konstruksi POSIX.

bash laporan-portabel.sh
#!/bin/sh
# laporan ringkas log sensor (subset POSIX)
berkas="${1:-sensor.log}"
if [ ! -f "$berkas" ]; then
    echo "berkas tidak ada: $berkas" >&2
    exit 1
fi
jumlah=$(grep -c "ERROR" "$berkas")
echo "jumlah baris ERROR: $jumlah"
text Keluaran
jumlah baris ERROR: 2

Menjalankan skrip yang sama dengan dua penafsir menghasilkan keluaran yang identik:

bash
# skrip portabel dijalankan dengan dua penafsir
bash laporan-portabel.sh sensor.log
dash laporan-portabel.sh sensor.log
text Keluaran
jumlah baris ERROR: 2
jumlah baris ERROR: 2

Konstruksi yang dipakai skrip di atas semuanya berada di lapis POSIX, yaitu uji kondisi [ ], substitusi perintah $( ), dan perluasan parameter ${1:-sensor.log} yang memberi nilai default pada argumen. Konstruksi yang perlu dihindari ketika targetnya penafsir POSIX antara lain array, [[ ]], substitusi proses <( ), dan opsi echo yang tidak standar seperti echo -e. Aturan praktisnya sederhana: tulis skrip dengan subset terkecil yang memenuhi kebutuhan, lalu verifikasi dengan menjalankannya pada penafsir POSIX sebelum dinyatakan portabel.

Kesalahan umum#

  • Skrip diuji hanya dengan bash tetapi dipasang dengan shebang #!/bin/sh. Penyebabnya adalah asumsi bahwa /bin/sh selalu bash, sedangkan pada Debian dan Ubuntu program itu adalah dash. Perbaikannya adalah menguji dengan dash skrip.sh atau mengubah shebang menjadi #!/usr/bin/env bash.
  • Fitur bash dipakai padahal targetnya penafsir lain. Array pada zsh berindeks mulai 1, sedangkan pada bash mulai 0, sehingga skrip bash yang dipindahkan ke zsh dapat membaca elemen yang salah. Perbaikannya adalah menyatakan tingkat portabilitas skrip sejak awal dan menaatinya secara konsisten.
  • Alias dipakai di dalam skrip. Shell non-interaktif tidak memperluas alias, sehingga skrip yang mengandalkan alias dari berkas konfigurasi gagal. Perbaikannya adalah menggunakan fungsi shell, yang tetap bekerja pada sesi non-interaktif.

Ringkasan#

  • POSIX.1-2017 menetapkan lapis bahasa shell minimum, dash menargetkan lapis itu saja, sedangkan bash dan zsh menambahkan fitur di atasnya dengan arah yang berbeda.
  • Shebang menentukan penafsir yang benar-benar menjalankan skrip, dan /bin/sh tidak selalu menunjuk bash.
  • Bashism seperti array dan [[ ]] gagal di dash dengan pesan Syntax error atau kode keluar 127.
  • Skrip portabel ditulis dalam subset POSIX dan diuji dengan dash maupun bash sebelum dipasang.

Referensi#