Strict mode

Flag set -euo pipefail dan pencegahan kesalahan yang dilakukan oleh masing-masing opsi.

Daftar isi

Skrip shell yang tidak menegakkan disiplin cenderung gagal dalam diam, yaitu tetap berjalan sampai akhir dan dilaporkan berhasil meskipun ada perintah yang mengalami error (keadaan gagal yang dilaporkan perintah atau program). Topik ini membahas strict mode, yakni kumpulan opsi yang dinyatakan pada awal skrip agar bash lebih ketat menilai kegagalan dan penggunaan variabel. Materinya mencakup tiga flag (opsi singkat yang mengubah perilaku perintah) pada builtin set (perintah yang disediakan langsung oleh shell), yaitu -e, -u, dan pipefail, beserta batasan yang menyertainya. Dua versi skrip arsip sederhana dipakai sebagai pembanding, satu versi rentan dan satu versi strict, sehingga perbedaan perilakunya dapat diamati langsung dari keluarannya.

Skrip yang gagal dalam diam#

Skrip pertama bertugas mengarsipkan satu berkas ke dalam direktori arsip. Nama berkasnya dibaca dari parameter posisional pertama, sedangkan nama direktorinya ditetapkan di dalam skrip. Strukturnya sengaja dibuat minimal, yaitu menyiapkan dua variabel, mencetak pesan, membuat direktori tujuan, menyalin berkasnya, dan mencatat bahwa pekerjaan selesai.

bash skrip-rapuh.sh
#!/usr/bin/env bash
# Skrip arsip tanpa strict mode: kesalahan lolos tanpa terdeteksi.

sumber="$1"
tujuan="arsip"

echo "Mengarsipkan $sumber ke $tujuan"
mkdir -p "$tujuan"
cp "$sumber" "$tujuan/"
echo "Selesai"
text Keluaran
Mengarsipkan  ke arsip
cp: cannot stat '': No such file or directory
Selesai

Keluarannya memuat tiga kejadian yang saling bertentangan. Pesan Mengarsipkan ke arsip tercetak dengan nama berkas yang kosong karena $1 tidak pernah diisi, dan cp jelas gagal dengan pesan cp: cannot stat ''. Meskipun demikian, baris Selesai tetap tercetak dan skrip berakhir dengan kode keluar 0, sehingga skrip lain yang memanggilnya tidak punya cara mengetahui bahwa arsipnya tidak pernah dibuat. Pola semacam ini paling berbahaya pada otomasi, karena kegagalannya baru ketahuan jauh setelah penyebabnya lewat.

Flag -e: berhenti saat perintah gagal#

Opsi -e membuat bash berhenti segera setelah ada perintah sederhana yang gagal, yaitu perintah yang berakhir dengan kode keluar bukan nol. Skrip kedua memakai versi strict dari logika yang sama, dan seluruh opsinya ditetapkan dalam satu baris set -euo pipefail.

bash skrip-strict.sh
# tidak-dijalankan
# Versi strict mode dari skrip-rapuh.sh.

set -euo pipefail

sumber="$1"
tujuan="arsip"

echo "Mengarsipkan $sumber ke $tujuan"
mkdir -p "$tujuan"
cp "$sumber" "$tujuan/"
echo "Selesai"

Baris pertama skrip ini bertanda # tidak-dijalankan karena isinya mengubah perilaku shell sesi. Karena itu, contoh pada topik ini selalu menjalankannya sebagai skrip anak melalui bash skrip-strict.sh, bukan dengan menyalin isinya ke sesi shell yang sedang dipakai. Dengan cara itu, strict mode bekerja di dalam skrip tanpa mengganggu sesi tempat percobaan lain berjalan.

bash
# jalankan dengan nama berkas yang tidak ada
bash skrip-strict.sh laporan-hilang.txt
echo "kode keluar: $?"
text Keluaran
Mengarsipkan laporan-hilang.txt ke arsip
cp: cannot stat 'laporan-hilang.txt': No such file or directory
kode keluar: 1

Kali ini skrip berhenti tepat pada perintah yang gagal. Pesan Mengarsipkan laporan-hilang.txt ke arsip sempat tercetak karena echo sebelum cp berhasil, tetapi baris Selesai tidak pernah muncul dan kode keluarnya 1. Kode keluar yang akurat seperti ini memungkinkan skrip pemanggil bereaksi, misalnya membatalkan proses cadangan dan memberi tahu pengguna.

bash
# buat berkas laporan.txt lalu arsipkan
printf 'laporan harian\nangka penjualan: 42\n' > laporan.txt
bash skrip-strict.sh laporan.txt
# periksa isi direktori tujuan
ls arsip
text Keluaran
Mengarsipkan laporan.txt ke arsip
Selesai
laporan.txt

Apabila masukannya benar, strict mode tidak mengubah perilaku yang sudah benar. Skrip berjalan sampai akhir, mencetak Selesai, dan ls arsip menunjukkan laporan.txt benar-benar ada di dalam direktori tujuan. Opsi -e dengan demikian tidak menambah pekerjaan pada jalur sukses, melainkan hanya memotong jalur yang salah.

Flag -u: variabel yang belum diberi nilai#

Opsi -u memperlakukan variabel atau parameter yang belum diberi nilai sebagai kesalahan, bukan sebagai teks kosong. Tanpa opsi ini, ekspansi $1 pada skrip tanpa argumen diam-diam menghasilkan teks kosong, dan itulah sebabnya versi rentan pada bagian pertama berhasil memanggil cp dengan nama berkas yang kosong. Ulangi pemanggilan skrip strict tanpa argumen untuk melihat perbedaannya.

bash
# jalankan tanpa argumen sama sekali
bash skrip-strict.sh
echo "kode keluar: $?"
text Keluaran
skrip-strict.sh: line 6: $1: unbound variable
kode keluar: 1

Pesan unbound variable menyebut baris tempat ekspansi terjadi, sehingga penyebabnya mudah dilacak. Kegagalan juga terjadi lebih dini daripada kasus -e, yaitu pada baris 6 sebelum mkdir dan cp sempat dieksekusi untuk pemanggilan tersebut. Dengan -u, kesalahan variabel yang belum diisi terdeteksi pada akarnya, bukan pada gejalanya.

Istilah unbound menggambarkan variabel yang belum diberi nilai sama sekali, berbeda dari variabel yang berisi teks kosong. Opsi -u hanya menangkap golongan pertama, sedangkan teks kosong tetap dianggap nilai yang sah. Parameter yang memang boleh kosong dinyatakan dengan nilai bawaan, misalnya ${1:-}, sehingga maksud skripnya tersurat dan tetap lolos dari -u.

Flag pipefail: kegagalan di tengah pipeline#

Sebuah pipeline (rangkaian perintah yang dihubungkan dengan pipe, yaitu mekanisme yang mengalirkan keluaran satu perintah menjadi masukan perintah berikutnya) secara default melaporkan kode keluar perintah terakhirnya saja. Kegagalan di tengah rangkaian dengan demikian tertutupi apabila perintah terakhirnya berhasil. Percobaan berikut memakai false yang selalu gagal dan true yang selalu berhasil.

bash
# periksa kode keluar pipeline tanpa pipefail
false | true
echo "kode keluar pipeline: $?"
text Keluaran
kode keluar pipeline: 0

Kode keluar pipeline tercatat 0 meskipun false di awalnya gagal, karena yang dinilai hanyalah true di ujung. Aktifkan pipefail lalu ulangi percobaan yang sama.

bash
# aktifkan pipefail lalu ulangi percobaan yang sama
set -o pipefail
false | true
echo "kode keluar pipeline: $?"
set +o pipefail
text Keluaran
kode keluar pipeline: 1

Setelah pipefail aktif, pipeline yang sama dilaporkan bernilai 1 karena salah satu anggotanya gagal. Opsi dimatikan kembali dengan set +o pipefail agar sesi tidak membawa efek sisa, karena opsi yang tertinggal mengubah perilaku perintah berikutnya tanpa jejak. Gabungan set -euo pipefail dengan demikian menutup tiga celah sekaligus, yaitu berhenti saat perintah gagal, menolak variabel yang belum diisi, dan menaikkan kegagalan dari dalam pipeline. Sebagai penutup, jalankan pemeriksa statis shellcheck atas kedua skrip untuk memastikan tidak ada temuan yang jelas pada keduanya.

bash
# pastikan kedua versi bebas dari temuan analisis statis
shellcheck skrip-rapuh.sh && echo 'meta: rapuh bersih'; shellcheck --shell=bash skrip-strict.sh && echo 'meta: strict bersih'
text Keluaran
meta: rapuh bersih
meta: strict bersih

Kegagalan yang tidak tertangkap -e#

Opsi -e memiliki batasan yang perlu dipahami sejak awal. Bash tidak menghentikan skrip apabila perintah yang gagal menjadi kondisi if atau while, berada di sisi kiri operator && atau ||, atau dinegasi dengan !. Perilaku ini disengaja, karena kondisi yang gagal justru merupakan masukan yang akan dinilai oleh percabangan, bukan kegagalan yang harus menghentikan segalanya. Akibat praktisnya, pemeriksaan yang ditulis sebagai perintah biasa di luar if dapat gagal tanpa efek apa pun.

Contohnya, baris grep pola berkas.txt yang ditulis langsung untuk memeriksa keberadaan pola akan gagal tanpa menghentikan apa pun bila polanya tidak ditemukan, sedangkan bentuk if grep -q pola berkas.txt; then menangani kasus yang sama secara tersurat. Pemeriksaan yang benar-benar kritis sebaiknya selalu ditulis sebagai kondisi if eksplisit yang cabangnya menangani kegagalan tersebut.

Kesalahan umum#

Kesalahan berikut paling sering muncul ketika strict mode baru mulai diterapkan, dan ketiganya berpola sama, yaitu opsi yang diterapkan pada tempat yang keliru.

  • Menyalin set -euo pipefail ke sesi terminal yang sedang aktif. Opsi -e menutup sesi tersebut pada perintah pertama yang gagal, sehingga pekerjaan lain ikut terganggu. Terapkan opsi ini di dalam berkas skrip, lalu jalankan skripnya dengan bash nama-skrip.sh.
  • Menempatkan baris set di tengah skrip. Perintah sebelum baris itu tetap berjalan dengan perilaku longgar, sehingga bagian paling awal justru yang paling rentan. Tulis set -euo pipefail tepat setelah shebang (baris #! yang menentukan penafsir skrip).
  • Menganggap -e sebagai pengganti validasi masukan. Opsi itu hanya bereaksi setelah ada perintah yang gagal, sedangkan pemeriksaan argumen dan berkas tetap ditulis sendiri secara eksplisit. Pisahkan kegagalan yang harus menghentikan skrip dari masukan yang cukup ditangani melalui percabangan.

Ringkasan#

  • set -e menghentikan skrip saat ada perintah yang gagal, sehingga kegagalan tidak lagi lolos tanpa terdeteksi.
  • set -u memperlakukan variabel yang belum diberi nilai sebagai kesalahan dan menyebut baris tempat ekspansinya terjadi.
  • set -o pipefail membuat kode keluar pipeline mengikuti perintah mana pun yang gagal, bukan hanya perintah terakhir.
  • Kegagalan di dalam kondisi if, sisi kiri && atau ||, dan negasi ! tidak memicu -e, sehingga pemeriksaan kritis tetap ditulis eksplisit.

Referensi#