Beberapa bab sebelumnya menyisakan janji yang sengaja ditunda ke bab ini. Bab 3 dan Bab 4 menyinggung opsi set -u yang membuat Bash "berhenti dengan error saat menemukan variabel belum terdefinisi", tapi baru dijelaskan tuntas di sini. Bab 5.2 menyebut kejanggalan variabel yang hilang begitu saja setelah pipeline selesai sebagai "topik yang akan disinggung lagi saat membahas trap dan debugging di Bab 25". Bab 8 menunda "penanganan error yang lebih matang", Bab 19 menunda mekanisme trap lengkap untuk cleanup, dan Bab 23 menunda pembahasan tuntas shellcheck. Sementara itu, Bab 14, 15, 20, 21, dan 24 sudah berulang kali memakai kombinasi set -euo pipefail di baris kedua tiap script tanpa pernah menjelaskan detail kerja tiap opsinya satu per satu. Bab ini menepati seluruh janji itu sekaligus, dibagi jadi empat bagian: mode debug lewat set -x, set -e, set -u, dan pipefail (25.1), trap untuk cleanup otomatis (25.2), menulis pesan error yang benar-benar membantu troubleshooting (25.3), dan memakai shellcheck sebagai jaring pengaman otomatis sebelum script masuk produksi (25.4).
25.1 Mode Debug: set -x, set -e, set -u, pipefail
Empat opsi ini adalah flag yang diaktifkan lewat builtin set, masing-masing mengubah perilaku shell saat menjalankan script. Menurut GNU Bash Reference Manual, -x, -e, dan -u bagian dari standar POSIX shell (bisa juga dipakai di sh murni), sementara pipefail adalah ekstensi yang dipopulerkan Korn shell (ksh) dan diadopsi Bash serta Zsh, tidak tersedia di implementasi sh POSIX yang paling minimal seperti dash. Lima sub-bab berikut membedah tiap opsi satu per satu, sebelum menyatukannya jadi satu kombinasi standar di 25.1.5.
25.1.1 Melacak Eksekusi dengan set -x
set -x mengaktifkan mode xtrace, yang menurut man bash mencetak setiap perintah beserta argumen yang sudah di-expand ke stderr, diawali karakter yang ditentukan variabel PS4 (defaultnya + ), persis sebelum perintah itu benar-benar dieksekusi. Ini paling berguna untuk melihat nilai variabel sesungguhnya saat runtime, bukan cuma menebak dari kode sumbernya.
#!/bin/bash
set -x
nama="Rudi"
jumlah=$((3 + 4))
echo "Halo, $nama! Total: $jumlah"Menjalankan script di atas mencetak setiap baris beserta hasil ekspansinya ke terminal:
+ nama=Rudi
+ jumlah=7
+ echo 'Halo, Rudi! Total: 7'
Halo, Rudi! Total: 7Baris + echo 'Halo, Rudi! Total: 7' menunjukkan variabel $nama dan $jumlah sudah tergantikan nilai sebenarnya sebelum echo dijalankan, detail yang tidak terlihat kalau cuma membaca kode sumber. Mode ini tidak wajib ditulis permanen di dalam script; cara yang lebih praktis untuk debugging sesaat adalah memanggilnya langsung dari command line tanpa mengubah shebang atau isi file sama sekali, mengikuti pola yang sama dengan bash --noprofile --norc -eo pipefail {0} yang dipakai GitHub Actions di balik layar (Bab 24.1.1).
bash -x nama-script.shUntuk script yang sudah dipecah jadi banyak file lewat source (topik Bab 26.1), PS4 layak dikustomisasi supaya jejak baris menunjukkan file dan nomor barisnya, bukan cuma tanda + polos.
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -xBASH_SOURCE dan LINENO adalah variabel bawaan Bash yang menyimpan nama file dan nomor baris skrip yang sedang dieksekusi, sesuai daftar variabel di GNU Bash Reference Manual; kombinasi ini jauh lebih cepat menunjuk lokasi pasti sebuah baris bermasalah ketimbang menyisir manual satu per satu. Satu peringatan penting yang sudah disinggung Bab 24.1.4 wajib diulang di sini: set -x mencetak nilai asli tiap variabel, termasuk kalau variabel itu kebetulan berisi password atau token API. Jangan pernah mengaktifkan -x pada bagian script yang memegang secret, apalagi di lingkungan CI/CD yang lognya bisa diakses banyak orang; kalau perlu trace di bagian tertentu saja, nyalakan dan matikan tepat di sekitar baris yang diperiksa lewat set -x diikuti set +x.
25.1.2 Menghentikan Script Otomatis dengan set -e
set -e (nama panjangnya errexit) membuat shell langsung keluar begitu sebuah pipeline, list, atau compound command selesai dengan status bukan nol, alih-alih melanjutkan ke baris berikutnya seolah tidak terjadi apa-apa, perilaku default Bash tanpa opsi ini.
#!/bin/bash
set -e
echo "Langkah 1"
false
echo "Langkah 2 (tidak akan pernah tercetak)"Perintah false di atas selalu keluar dengan status 1, dan set -e langsung menghentikan script tepat di baris itu, tidak pernah sampai ke echo "Langkah 2". Menurut GNU Bash Reference Manual, -e punya sejumlah pengecualian yang wajib dihafal supaya perilakunya tidak mengejutkan: kegagalan tidak menghentikan script kalau perintah itu bagian dari kondisi if/while/until, bagian dari daftar && atau || selain perintah paling akhir, dinegasikan dengan !, atau bagian dari pipeline selain perintah paling akhir (kecuali pipefail ikut diaktifkan, dibahas di 25.1.4).
Pengecualian pertama itu persis menjelaskan kenapa pola idiomatik di Bab 23.2.3 tetap aman dipakai bersama set -e: id "$user" yang keluar dengan status bukan nol kalau user belum ada tidak pernah menghentikan script, karena perintah itu diuji langsung sebagai kondisi if.
if id "$user" &> /dev/null; then
echo "User $user sudah ada, lewati"
else
useradd -m -s /bin/bash "$user"
fiSebaliknya, kalau baris useradd -m -s /bin/bash "$user" dipanggil langsung tanpa pembungkus if pada user yang kebetulan sudah ada, exit status 9 yang menurut dokumentasi resmi useradd menandakan "username or group name already in use" akan langsung menghentikan seluruh script di bawah set -e, persis peringatan yang sudah disinggung Bab 23.2.3. Inilah alasan kenapa fungsi pembuatan user idempoten di bab itu wajib membungkus pemanggilan useradd di dalam percabangan eksplisit, bukan sekadar mengandalkan set -e untuk "menangkap" kegagalan yang sebenarnya bukan kegagalan fatal.
Jebakan kedua yang jauh lebih sering luput dari perhatian: exit status dari command substitution yang langsung digabung dengan deklarasi local ikut tertutupi oleh exit status local itu sendiri.
set -e
ambil_status() {
local hasil=$(curl -sf "https://api.example.com/health")
echo "$hasil"
}Kalau curl di atas gagal (misalnya endpoint mati), set -e tetap tidak menghentikan script, karena exit status yang benar-benar diperiksa adalah exit status perintah local, yang hampir selalu 0, bukan exit status curl di dalam $(...). ShellCheck menandai pola ini secara spesifik lewat kode SC2155 ("Declare and assign separately to avoid masking return values"), dan solusinya memang memisahkan deklarasi dari assignment jadi dua baris terpisah.
ambil_status() {
local hasil
hasil=$(curl -sf "https://api.example.com/health")
echo "$hasil"
}25.1.3 Mendeteksi Variabel Kosong dengan set -u
set -u (nama panjangnya nounset) mengubah perilaku Bash saat menemukan variabel yang belum pernah di-set: alih-alih diam-diam memperlakukannya sebagai string kosong seperti default Bash, shell mencetak pesan error ke stderr dan langsung keluar dengan status bukan nol, sesuai definisi resminya di GNU Bash Reference Manual.
#!/bin/bash
set -u
echo "$NAMA_TYPO"nama-script.sh: line 4: NAMA_TYPO: unbound variableIni persis skenario yang disinggung Bab 3: typo nama variabel yang tanpa set -u lolos begitu saja sebagai string kosong, menghasilkan bug yang membingungkan karena tidak ada pesan error sama sekali. Opsi ini juga menuntaskan janji Bab 4 soal mengakses index array yang belum pernah diisi.
declare -a arr=(a b c)
set -u
echo "${arr[10]}"nama-script.sh: line 3: arr[10]: unbound variableBab 4 menunjukkan bahwa tanpa set -u, mengakses ${arr[10]} pada array yang cuma berisi tiga elemen hanya mencetak string kosong tanpa peringatan apa pun; dengan set -u aktif, kesalahan index seperti ini langsung ketahuan saat runtime alih-alih diam-diam menghasilkan output yang salah. Satu pengecualian penting perlu dicatat: sejak Bash 4.4, ekspansi "${arr[@]}" pada array yang memang kosong (bukan sekadar satu index yang belum diisi) dikecualikan dari pengecekan nounset, sesuai catatan resmi di berkas CHANGES yang menyertai rilis Bash tersebut; untuk versi Bash yang lebih lama dari 4.4, ekspansi array kosong dalam mode set -u bisa memicu error yang sama.
Pola ${VAR:-default} dari Bab 6.4.4, yang sudah berulang kali dipakai di sepanjang series ini (misalnya "${1:-help}" di dispatcher Bab 24.3.2), sengaja dirancang supaya tidak memicu error nounset sekalipun VAR belum pernah di-set sama sekali; parameter expansion jenis ini termasuk pengecualian resmi yang didefinisikan langsung di dalam sintaksnya sendiri, bukan celah dari set -u.
25.1.4 Menangkap Kegagalan Pipeline dengan pipefail
Secara default, exit status sebuah pipeline (rangkaian perintah yang disambung |, Bab 5.3) cuma diwakili oleh exit status perintah paling terakhir, apa pun yang terjadi di perintah-perintah sebelumnya di dalam pipe yang sama.
false | true
echo "$?"
# 0, padahal "false" di awal pipe jelas gagalset -o pipefail mengubah aturan itu; menurut GNU Bash Reference Manual, dengan opsi ini aktif, exit status pipeline jadi exit status perintah paling kanan yang keluar dengan status bukan nol, atau nol kalau seluruh perintah di dalam pipe berhasil.
set -o pipefail
false | true
echo "$?"
# 1, sekarang kegagalan "false" ikut terdeteksiKegunaan opsi ini paling terasa di skenario yang sudah disinggung Bab 24.1.1: pipeline seperti curl ... | grep sukses yang gagal di tahap curl (misalnya jaringan putus) tanpa pipefail tetap dianggap "berhasil" oleh langkah CI/CD, karena exit status yang diperiksa cuma milik grep di ujung pipe, padahal grep sendiri bisa saja kebetulan tidak menemukan pola apa pun di output kosong dan tetap keluar dengan status bukan nol, atau sebaliknya kebetulan cocok dengan output error yang tidak relevan. Tanpa pipefail, kegagalan di tengah pipe jenis ini gampang lolos tanpa terdeteksi sampai efeknya baru terasa jauh di langkah berikutnya.
25.1.5 Kombinasi set -euo pipefail dan Pengecualiannya
Kombinasi keempat opsi di atas, biasanya ditulis set -euo pipefail di baris kedua script (tepat setelah shebang), sudah dipakai berulang kali di sepanjang series ini, mulai dari Bab 14 dan 15 sampai Bab 21 dan 24. Tiga huruf e, u, dan o pada perintah itu sebenarnya cuma singkatan gabungan untuk -e dan -u, sementara pipefail tetap harus ditulis lewat -o pipefail karena bukan opsi satu huruf; kombinasi itu setara persis dengan menulis tiga baris terpisah:
set -e
set -u
set -o pipefailKombinasi ini dikenal luas di kalangan penulis script sebagai bash strict mode, istilah tidak resmi yang populer lewat berbagai tulisan komunitas dan tidak muncul dengan nama itu di GNU Bash Reference Manual sendiri; nama itu sekadar label praktis untuk kebiasaan menyalakan ketiga pengaman sekaligus di awal script.
| Opsi | Efek | Sumber | Pengecualian utama |
|---|---|---|---|
set -x | Cetak tiap perintah ke stderr sebelum dieksekusi | POSIX | Bisa membocorkan secret ke log (Bab 24.1.4) |
set -e | Keluar segera saat perintah gagal | POSIX | Kondisi if/while/&&/||, dan bukan perintah terakhir di pipe |
set -u | Anggap variabel belum di-set sebagai error | POSIX | ${var:-default} dan sejenisnya |
set -o pipefail | Exit status pipeline ikut memperhitungkan kegagalan di tengah pipe | Ekstensi bash/ksh/zsh | Tidak aktif secara default, tidak tersedia di dash |
Bukan kebetulan Bab 20.4.2 dan Bab 21.4.2 justru sama-sama sengaja tidak memakai -e, cukup set -uo pipefail: alert-notifikasi.sh di Bab 20.4.2 butuh cek_service tetap sempat berjalan sekalipun cek_disk di atasnya gagal duluan, dan deploy-pipeline.sh di Bab 21.4.2 butuh logika rollback di bagian akhirnya tetap berjalan sekalipun langkah restart sebelumnya "gagal" secara exit status, sesuatu yang langsung hilang kalau -e ikut diaktifkan. Bandingkan dengan deploy.sh di Bab 21.3.2 dan provision-server.sh di Bab 24.4.1 yang justru memakai set -euo pipefail penuh, karena keduanya memang tidak punya logika lanjutan yang sengaja perlu tetap jalan setelah satu langkah gagal, jadi kegagalan di tengah prosesnya (misalnya git fetch putus atau paket gagal terpasang) seharusnya langsung menghentikan seluruh script, bukan melanjutkan di atas fondasi yang sudah diketahui rusak. Keputusan memakai -e penuh atau tidak bukan soal mana yang "lebih aman" secara mutlak, melainkan soal apakah script itu masih punya langkah pembersihan atau pemulihan yang wajib sempat berjalan sekalipun ada bagian yang gagal, keputusan yang cuma bisa diambil dengan memahami logika bisnis script itu sendiri.
Satu detail lanjutan yang layak diketahui untuk Bash 4.4 ke atas: opsi shopt -s inherit_errexit membuat command substitution ($(...)) ikut mewarisi status errexit dari shell induknya, alih-alih otomatis dinonaktifkan di dalam subshell command substitution seperti perilaku default, sesuai dokumentasi resmi shopt di GNU Bash Reference Manual. Tanpa opsi tambahan ini, kegagalan perintah di dalam $(...) pada Bash versi lama bisa lolos tanpa terdeteksi sekalipun set -e sudah aktif di luar; menambahkan shopt -s inherit_errexit tepat setelah set -euo pipefail menutup celah itu untuk Bash 4.4 ke atas.
25.1.6 Kesalahan Umum: Lupa -o dan Retrofit ke Script Lama
Dua kesalahan berikut paling sering ditemui di lapangan saat mode debug ini mulai dipakai, keduanya sama-sama tidak memicu error sintaks apa pun sehingga gampang lolos tanpa disadari. Pertama, menulis set -e pipefail tanpa -o di depan pipefail. Menurut spesifikasi resmi set di POSIX Shell Command Language, argumen apa pun setelah opsi terakhir yang tidak diawali tanda hubung diperlakukan sebagai positional parameter baru, bukan opsi tambahan.
set -e pipefail
echo "$1"
# mencetak "pipefail", bukan mengaktifkan opsi pipefail sama sekaliBaris set -e pipefail di atas cuma mengaktifkan -e, sementara kata pipefail diam-diam menjadi nilai baru untuk $1, menimpa argumen asli yang mungkin sudah diterima script lewat command line, tanpa satu pun pesan error yang menandakan kesalahan ini. Bentuk yang benar tetap set -eo pipefail atau set -e; set -o pipefail sebagai dua baris terpisah.
Kesalahan kedua muncul saat set -e ditambahkan ke script lama yang sebelumnya tidak pernah memakainya. Banyak perintah lazim keluar dengan status bukan nol dalam kondisi yang sebenarnya masih wajar, misalnya grep yang keluar status 1 kalau polanya memang tidak ditemukan (bukan berarti grep-nya gagal dieksekusi), dan retrofit -e ke script yang punya baris seperti itu tanpa pembungkus if membuat script berhenti mendadak di titik yang sebelumnya dianggap normal. Menambahkan set -euo pipefail ke script yang sudah berjalan lama di produksi sebaiknya tidak dilakukan sekali gebrak lalu langsung dianggap selesai; jalankan dulu skenario uji yang mewakili kondisi normal maupun kondisi tepi (input kosong, pencarian yang tidak ketemu, koneksi yang gagal), amati baris mana yang berhenti tidak semestinya, lalu bungkus khusus baris itu dengan if atau tambahkan || true kalau memang statusnya boleh diabaikan, bukan langsung mencopot -e lagi begitu satu baris bermasalah ditemukan.
25.2 Trap dan Cleanup dengan trap ... EXIT
Empat opsi set di 25.1 bagus untuk mendeteksi dan menghentikan script begitu ada yang salah, tapi penghentian mendadak punya risiko sendiri: file sementara yang terlanjur dibuat, lock yang terlanjur dipegang, atau koneksi yang terlanjur dibuka bisa saja tertinggal begitu saja kalau script berhenti di tengah jalan. Builtin trap menutup celah itu dengan menjalankan perintah tertentu tepat sebelum shell benar-benar berhenti, apa pun penyebabnya.
25.2.1 Cara Kerja trap dan Sinyal di Bash
Menurut man bash, sintaks dasar trap adalah trap [-lp] [arg] [sigspec ...]: begitu shell menerima salah satu sigspec yang didaftarkan, perintah arg dibaca dan dieksekusi. sigspec bisa berupa nama signal POSIX standar seperti INT (sinyal interupsi, dikirim saat menekan Ctrl+C, bernomor 2) atau TERM (sinyal permintaan berhenti secara normal, dikirim kill tanpa opsi tambahan, bernomor 15), sesuai definisi resmi di man page signal(7) pada man7.org. Selain sinyal sungguhan itu, Bash juga menyediakan beberapa pseudo-signal khusus: EXIT yang terpicu saat shell keluar apa pun sebabnya, ERR yang terpicu saat perintah gagal (tunduk pada pengecualian yang sama dengan set -e di 25.1.2), dan DEBUG yang terpicu sebelum tiap perintah sederhana dieksekusi.
Dua sinyal, SIGKILL (nomor 9) dan SIGSTOP, sama sekali tidak bisa ditangkap, diblokir, atau diabaikan oleh proses apa pun, termasuk Bash; ini bukan batasan Bash melainkan aturan level kernel yang berlaku universal, ditegaskan baik oleh GNU Bash Reference Manual maupun man page signal(7). Konsekuensi praktisnya: kill -9 pada sebuah script tidak akan pernah memicu trap apa pun yang sudah didaftarkan, script langsung dihentikan paksa oleh kernel tanpa sempat menjalankan satu baris cleanup pun. ShellCheck bahkan punya kode khusus untuk mengingatkan kekeliruan ini, SC2173 ("SIGKILL/SIGSTOP can not be trapped"), yang muncul begitu ada script yang mencoba menulis trap ... KILL atau trap ... STOP, sesuatu yang tidak akan pernah berfungsi apa pun isi perintahnya. trap -p berguna untuk memeriksa daftar trap yang sedang aktif di sebuah shell, cara cepat memastikan trap yang dimaksud benar-benar terpasang saat men-debug script yang rumit.
25.2.2 Membersihkan File Sementara dengan trap ... EXIT
Skenario paling umum yang butuh trap: sebuah script membuat file sementara lewat mktemp, dan file itu wajib terhapus tidak peduli apakah script selesai normal, berhenti karena set -e, atau dihentikan pengguna di tengah jalan.
#!/bin/bash
set -euo pipefail
TMP_FILE=$(mktemp)
trap 'rm -f "$TMP_FILE"' EXIT
echo "Menulis data sementara ke $TMP_FILE"
echo "data penting" > "$TMP_FILE"
# proses lain yang memakai $TMP_FILE, termasuk yang mungkin gagal
grep "penting" "$TMP_FILE"Baris trap 'rm -f "$TMP_FILE"' EXIT didaftarkan tepat setelah TMP_FILE dibuat, sebelum baris mana pun yang berpotensi gagal. Kalau grep di baris terakhir kebetulan tidak menemukan pola dan keluar dengan status bukan nol, set -e langsung menghentikan script, tapi trap EXIT tetap sempat berjalan lebih dulu sebelum shell benar-benar berhenti, sehingga $TMP_FILE tetap terhapus. Ini menjawab tuntas kekhawatiran soal cleanup yang gagal berjalan gara-gara set -e menghentikan script secara tiba-tiba: kombinasi set -euo pipefail dan trap ... EXIT justru saling melengkapi, bukan saling bertentangan.
25.2.3 Menangani SIGINT dan SIGTERM Secara Eksplisit
Bab 19.4.2 memperkenalkan trap sederhana untuk dashboard monitoring, trap 'printf "\nDashboard dihentikan.\n"; exit 0' INT, cukup untuk kebutuhan saat itu karena dashboard tersebut tidak memegang resource apa pun selain tampilan di layar terminal. Begitu sebuah script mulai memegang resource nyata seperti file sementara atau koneksi, trap tunggal untuk INT saja tidak lagi cukup, karena proses yang sama bisa juga dihentikan lewat SIGTERM (misalnya oleh systemd saat service di-stop, atau oleh kill tanpa opsi tambahan). Cara paling eksplisit dan aman menjamin cleanup berjalan di ketiga skenario, keluar normal, dihentikan Ctrl+C, maupun dihentikan SIGTERM, adalah mendaftarkan trap yang sama untuk EXIT sekaligus INT dan TERM dalam satu pemanggilan, bukan mengandalkan asumsi bahwa satu trap saja otomatis mencakup sinyal lainnya.
#!/bin/bash
set -euo pipefail
TMP_FILE=$(mktemp)
cleanup() {
local kode_keluar=$?
printf '\nMembersihkan file sementara...\n'
rm -f "$TMP_FILE"
exit "$kode_keluar"
}
trap cleanup EXIT INT TERM
echo "Bekerja dengan $TMP_FILE, tekan Ctrl+C untuk menguji cleanup"
sleep 30Menurut GNU Bash Reference Manual, mendaftarkan lebih dari satu sigspec pada satu pemanggilan trap membuat perintah arg yang sama berlaku untuk seluruh sinyal yang disebut, jadi cleanup EXIT INT TERM di atas memasang fungsi cleanup yang identik untuk ketiganya, tidak perlu tiga baris trap terpisah. Baris local kode_keluar=$? di awal fungsi merekam exit status yang berlaku tepat sebelum trap terpicu, supaya exit "$kode_keluar" di baris terakhir tetap meneruskan kode keluar yang sebenarnya, bukan menimpanya jadi 0 begitu saja. Untuk skrip yang exit code-nya perlu mencerminkan sinyal spesifik yang menghentikannya, konvensi yang didokumentasikan resmi di GNU Bash Reference Manual bagian "Exit Status" adalah 128 + nomor sinyal, sehingga Ctrl+C (SIGINT, nomor 2) lazim menghasilkan exit code 130, dan SIGTERM (nomor 15) menghasilkan 143; daftar lengkap kode exit dan artinya ada di Lampiran B.
25.2.4 Kapan Trap Tidak Diperlukan
Trap bukan solusi cleanup universal untuk semua resource. Bab 23.2.4 memperlihatkan pola exec 200>/var/lock/toolkit-keamanan.lock yang dikombinasikan dengan flock -n 200, dan sudah menegaskan lock itu "otomatis terlepas begitu script selesai atau proses berakhir paksa sekalipun, karena kernel melepas file lock begitu file descriptor pemiliknya ditutup, jadi tidak perlu trap tambahan khusus untuk membersihkannya". Perbedaan mendasarnya ada pada siapa yang bertanggung jawab atas resource itu: sebuah file descriptor dan lock yang menempel padanya dikelola langsung oleh kernel dan otomatis dibersihkan begitu proses berakhir apa pun sebabnya, sementara sebuah file biasa di disk (seperti hasil mktemp di 25.2.2) tetap ada di filesystem sampai ada yang menghapusnya secara eksplisit, kernel tidak pernah tahu file itu "sementara" dan tidak akan menghapusnya sendiri.
Aturan praktis untuk menentukan perlu tidaknya trap: kalau resource-nya otomatis dilepas kernel saat proses berakhir (file descriptor, lock berbasis flock, koneksi socket yang menutup sendiri saat proses mati), trap tambahan cuma menambah kompleksitas tanpa manfaat nyata. Kalau resource-nya berupa jejak yang tetap bertahan di filesystem atau di luar siklus hidup proses itu sendiri (file sementara, baris di authorized_keys, entri di database), trap wajib dipasang supaya jejak itu tidak tertinggal begitu saja.
25.3 Menulis Pesan Error yang Informatif
Mode debug di 25.1 dan trap di 25.2 membantu script menangani kegagalan dengan benar secara teknis, tapi keduanya tidak otomatis membuat pesan yang dilihat manusia jadi lebih jelas. Bagian ini menutup celah itu: bagaimana pesan error ditulis supaya orang yang membacanya (bisa jadi Sysadmin lain, bisa jadi diri sendiri enam bulan kemudian) langsung tahu apa yang salah tanpa harus membaca ulang seluruh kode sumber.
25.3.1 Ciri Pesan Error yang Membantu Troubleshooting
Pesan error yang baik memuat tiga hal sekaligus: konteks (apa yang sedang dilakukan script saat gagal), penyebab (kalau bisa dipastikan), dan langkah perbaikan (kalau ada yang bisa disarankan). Bandingkan dua pendekatan berikut untuk kasus file konfigurasi yang hilang.
# Kurang informatif
echo "Error" >&2
exit 1
# Jauh lebih membantu
echo "Error: file konfigurasi '$CONFIG_FILE' tidak ditemukan." >&2
echo "Jalankan './setup.sh' terlebih dahulu untuk membuatnya." >&2
exit 1Detail teknis yang wajib diikuti pada kedua versi di atas: pesan error selalu diarahkan ke stderr lewat >&2, mengikuti pemisahan stdout/stderr yang sudah ditekankan sejak Bab 5.2. Ini bukan sekadar gaya penulisan, tapi kebutuhan fungsional: begitu sebuah script dipakai di dalam pipeline (Bab 5.3) atau hasil stdout-nya ditangkap lewat command substitution (Bab 3.4), pesan error yang salah arah ke stdout akan ikut tercampur ke data yang sedang diproses skrip pemanggilnya, mengacaukan parsing di sisi penerima tanpa ada tanda peringatan apa pun.
25.3.2 Fungsi error() dan die() Terpusat
Menulis echo "Error: ..." >&2 berulang-ulang di banyak tempat gampang jadi tidak konsisten, kadang lupa >&2, kadang lupa format pesan. Pola yang lebih rapi memakai konsep fungsi dari Bab 8 untuk menyatukan seluruh pemanggilan error lewat satu titik.
error() {
printf 'Error: %s\n' "$1" >&2
}
die() {
error "$1"
exit "${2:-1}"
}die() di atas memakai kembali error(), pola memanggil fungsi dari dalam fungsi lain yang sudah dikenalkan sejak Bab 8.1.2, dipakai di sini supaya format pesan cukup didefinisikan sekali lalu dipakai ulang. Parameter kedua yang opsional ("${2:-1}", memakai nilai default dari Bab 6.4.4) memungkinkan pemanggil menentukan exit code spesifik kalau diperlukan, sambil tetap jatuh ke 1 sebagai default yang wajar. Pemakaiannya jadi jauh lebih ringkas dibanding menulis ulang echo ... >&2; exit 1 di setiap titik validasi.
[[ -f "$CONFIG_FILE" ]] || die "File konfigurasi '$CONFIG_FILE' tidak ditemukan"
[[ -n "${API_KEY:-}" ]] || die "Variabel API_KEY wajib di-set" 2Karena dua fungsi ini dipakai berulang di banyak script berbeda (persis seperti pola log() di Bab 19.2 atau jalankan_jauh() di Bab 24.3.3), fungsi ini adalah kandidat ideal untuk dipindah ke satu file lib.sh bersama yang di-source dari tiap script, pola pengorganisasian yang dibahas lebih lanjut di Bab 26.1, alih-alih disalin ulang manual ke setiap script baru.
25.3.3 Exit Code yang Bermakna
Bab 6.4 sudah membahas $? sebagai exit status perintah terakhir, dan Lampiran B series ini merangkum daftar exit code beserta artinya. Beberapa konvensi yang layak dipegang saat menentukan exit code kustom pada script sendiri: 0 selalu berarti sukses, sementara nilai bukan nol menandakan kegagalan dalam berbagai bentuk. Menurut TLDP Advanced Bash-Scripting Guide, kode 126 secara konvensi dipakai shell untuk menandai perintah ditemukan tapi tidak bisa dieksekusi (misalnya masalah permission), dan 127 untuk perintah yang sama sekali tidak ditemukan; kode 128 + nomor sinyal untuk proses yang dihentikan sinyal sudah dibahas di 25.2.3 dan didokumentasikan resmi di GNU Bash Reference Manual.
Satu batasan teknis yang wajib diketahui: exit status di shell POSIX (termasuk Bash) selalu berada di rentang 0 sampai 255, karena nilainya disimpan sebagai bilangan 8-bit di level sistem operasi. Konsekuensinya, exit 256 tidak menghasilkan error, melainkan "membungkus" balik jadi exit status 0, dan exit 257 jadi 1, gara-gara operasi modulo 256 yang terjadi diam-diam di balik layar. Menetapkan exit code kustom yang lebih besar dari 255 pada script sendiri tidak pernah menghasilkan efek yang diharapkan, dan sebaiknya dihindari sama sekali.
Untuk script yang dipanggil dari script lain (seperti ops.sh memanggil deploy-pipeline.sh di Bab 24.3.3), exit code yang berbeda-beda untuk kelas kegagalan yang berbeda memudahkan script pemanggil mengambil keputusan tanpa perlu mem-parsing teks pesan error.
EXIT_ARGUMEN_SALAH=2
EXIT_DEPENDENSI_HILANG=3
EXIT_JARINGAN_GAGAL=4
[[ -n "${1:-}" ]] || die "Argumen wajib diisi" "$EXIT_ARGUMEN_SALAH"
command -v curl &> /dev/null || die "curl tidak ditemukan, pasang dulu" "$EXIT_DEPENDENSI_HILANG"Pola ini membuat script pemanggil bisa memeriksa $? setelah pemanggilan dan bercabang sesuai kelas kegagalan, misalnya mencoba ulang khusus untuk EXIT_JARINGAN_GAGAL tapi langsung menyerah untuk EXIT_ARGUMEN_SALAH yang tidak akan pernah berhasil sekalipun diulang berkali-kali.
25.4 ShellCheck untuk Linting Script
Tiga bagian sebelumnya membahas cara menulis script yang menangani error dengan baik, tapi seluruhnya masih bergantung pada kedisiplinan manusia untuk konsisten menerapkannya di setiap baris. shellcheck mengotomatiskan sebagian besar pengecekan itu lewat static analysis, teknik memeriksa kode tanpa benar-benar menjalankannya, dan sudah disinggung sejak Bab 23.1.4 sebagai "cara tercepat memverifikasi kebiasaan quoting dan pola berbahaya secara otomatis".
25.4.1 Instalasi dan Pemakaian Dasar ShellCheck
ShellCheck tersedia lewat package manager di hampir semua distro dan platform utama series ini, konsisten dengan cakupan lintas OS yang dibahas Bab 1.3: apt install shellcheck di Debian/Ubuntu, dnf install ShellCheck di Fedora, brew install shellcheck di macOS lewat Homebrew, atau mengunduh binary statis langsung dari halaman rilis resmi koalaman/shellcheck di GitHub untuk sistem yang tidak menyediakan package tersebut. Pastikan instalasi berhasil lewat shellcheck --version sebelum lanjut memakainya; perintah ini menampilkan versi ShellCheck sekaligus versi Bash yang jadi target analisisnya secara default.
Contoh script berikut sengaja memuat beberapa kesalahan umum yang mengulang tema dari Bab 23.1 soal command injection dan unquoted variable, kali ini diverifikasi lewat shellcheck alih-alih pemeriksaan manual.
#!/bin/bash
BACKUP_DIR=$1
cd $BACKUP_DIR
tar -czf backup.tar.gz *shellcheck backup.shIn backup.sh line 3:
cd $BACKUP_DIR
^-- SC2086 (info): Double quote to prevent globbing and word splitting.
In backup.sh line 3:
cd $BACKUP_DIR
^-- SC2164 (warning): Use 'cd ... || exit' in case cd fails.
In backup.sh line 4:
tar -czf backup.tar.gz *
^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options.Ketiga temuan ini saling melengkapi dan seluruhnya sudah pernah disinggung di bab-bab sebelumnya secara terpisah: SC2086 menegakkan kebiasaan quoting variabel dari Bab 23.1.1, SC2164 mengingatkan bahwa cd yang gagal tanpa penanganan bisa membuat baris berikutnya berjalan di direktori yang salah (skenario yang sangat berbahaya kalau baris berikutnya berisi operasi destruktif), dan SC2035 menegakkan kebiasaan menambahkan -- atau prefix ./ sebelum glob dari Bab 23.1.4, supaya nama file yang sengaja dibuat diawali tanda hubung oleh Attacker tidak disalahartikan sebagai opsi command line oleh tar.
25.4.2 Membaca Kode SC dan Tingkat Severity
Setiap temuan ShellCheck punya kode unik berformat SCxxxx, dan tiap kode itu punya halaman khusus di ShellCheck wiki (shellcheck.net/wiki/SCxxxx) yang menjelaskan alasan di balik aturannya beserta contoh kode yang benar dan salah, jauh lebih detail dibanding satu baris pesan yang tampil di terminal. Menurut dokumentasi resminya, ShellCheck mengelompokkan temuan ke empat tingkat severity: error untuk masalah yang hampir pasti bug, warning untuk pola berisiko yang layak diperbaiki, info untuk saran perbaikan gaya yang tidak sepenuhnya wajib, dan style untuk preferensi penulisan kode yang paling ringan bobotnya.
Opsi -S/--severity membatasi output cuma ke tingkat tertentu ke atas, berguna untuk menyaring temuan minor saat fokus dulu ke masalah paling kritis.
shellcheck --severity=warning backup.shPerintah di atas cuma menampilkan temuan bertingkat warning dan error, mengabaikan SC2086 dan SC2035 yang levelnya info pada contoh 25.4.1. Untuk temuan yang sudah sengaja diabaikan dengan alasan yang dipahami (misalnya variabel yang memang aman tidak di-quote karena isinya sudah divalidasi ketat sebelumnya), tambahkan komentar direktif tepat di atas baris yang dimaksud, bukan mematikan pengecekan itu secara global untuk seluruh file.
# shellcheck disable=SC2086
echo $VARIABEL_YANG_SUDAH_DIVALIDASI25.4.3 Mengintegrasikan ShellCheck ke Editor dan CI/CD
Menjalankan shellcheck manual satu per satu gampang terlewat begitu jumlah script di sebuah proyek mulai menumpuk seperti kondisi di akhir Bab 24 (deploy-pipeline.sh, ops.sh, provision-server.sh). Dua tempat yang paling efektif memasang ShellCheck sebagai jaring pengaman otomatis: editor kode, lewat ekstensi ShellCheck resmi di VS Code atau plugin sejenis di Vim/Neovim yang memanggil engine yang sama, sehingga temuan langsung muncul begitu baris kode diketik; dan pipeline CI/CD dari Bab 24.1, sebagai langkah terpisah sebelum job deploy benar-benar berjalan.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Lint script dengan ShellCheck
run: shellcheck deploy-pipeline.sh ops.sh provision-server.sh
deploy:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Deploy lewat SSH
run: echo "Deploy berjalan setelah lint lolos"Runner ubuntu-latest pada GitHub Actions sudah menyertakan shellcheck terpasang bawaan, sesuai daftar software resmi di repository actions/runner-images (per Ubuntu 24.04, tercatat versi 0.9.0), sehingga langkah lint di atas tidak perlu instalasi tambahan sama sekali; tetap cek ulang daftar software runner yang dipakai kalau perilakunya kelak berubah, karena versi dan paket bawaan tiap image bisa saja diperbarui dari waktu ke waktu. Kunci integrasi ini ada pada needs: lint di job deploy: menurut dokumentasi resmi GitHub Actions, job dengan needs menunggu job yang disebut selesai lebih dulu, dan kalau job itu gagal, job yang bergantung padanya otomatis ikut dilewati, tidak pernah dijalankan sama sekali. ShellCheck sendiri keluar dengan exit status 1 begitu menemukan minimal satu temuan (dengan tingkat severity default), sesuai dokumentasi resminya; dikombinasikan dengan set -e yang jadi perilaku default langkah run GitHub Actions (Bab 24.1.1), satu baris kode yang lolos quoting saja sudah cukup menghentikan seluruh job lint, dan secara otomatis mencegah job deploy ikut berjalan di atas kode yang belum lolos pemeriksaan dasar.
Bab ini menutup pembahasan mendetail soal mode debug, trap, pesan error, dan linting, empat kemampuan yang selama ini cuma disinggung sekilas di banyak bab sebelumnya. Fungsi error() dan die() dari 25.3.2, ditambah pola cleanup() dari 25.2.3, sekarang jadi kumpulan kebiasaan siap pakai yang layak disatukan jadi satu file utilitas bersama, bukan disalin ulang manual ke setiap script baru. Itulah persis yang dibahas Bab 26 berikutnya, mengorganisasi script yang jumlahnya sudah menumpuk lewat struktur multi-file dan source, dilengkapi dokumentasi pemakaian dan pengujian otomatis lewat bats, supaya seluruh kebiasaan baik yang sudah dibangun sepanjang Bagian 3 dan Bab 25 ini tetap konsisten dipakai di proyek yang terus berkembang.

