Skrip yang salah perilaku perlu dibedah, bukan ditebak. Episode ini membahas bash -x untuk menelusuri eksekusi, bash -n untuk memeriksa sintaks, PS4 untuk jejak yang kaya konteks, serta trap DEBUG sebagai breakpoint manual. Diakhiri praktik debug skrip yang sengaja rusak dari awal sampai ketemu akar masalah.

Setelah di episode 19 sebelumnya kita membahas Signals Handling & Traps — bagaimana skrip menangkap sinyal dan membersihkan diri ketika keluar — kalian sekarang punya skrip yang tidak hanya berhenti dengan jujur, tetapi juga meninggalkan ruangan dengan rapi. Namun ada satu momen yang tak terhindarkan dalam perjalanan setiap programmer: skrip berjalan, tetapi hasilnya salah. Tidak ada error, tidak ada crash — hanya perilaku yang tidak sesuai harapan. File yang seharusnya dibuat tidak ada, nilai yang seharusnya terisi kosong, atau urutan yang seharusnya terjadi tidak terjadi.
Di titik ini, banyak orang mulai menebak-nebak: menambah echo di sana-sini, mencoba-coba mengubah urutan, berdoa kepada dewa terminal. Pendekatan coba-coba seperti itu lambat, membuat frustrasi, dan — yang paling berbahaya — sering kali memperbaiki gejala tanpa menemukan akar masalah. Persis seperti mekanik yang mengganti busi padahal yang rusak kabel pengapian: mobil sebentar jalan, lalu rusak lagi.
Episode 20 ini adalah kursus otopsi skrip: teknik-teknik sistematis untuk melihat apa yang benar-benar dilakukan BASH, bukan apa yang kalian bayangkan dilakukannya. Kita akan membedah bash -x (mode trace), bash -n (pemeriksaan sintaks), bash -v (verbose), PS4 untuk memperkaya jejak dengan nama file, nomor baris, dan nama fungsi, serta trap ... DEBUG sebagai breakpoint manual ala printf-debugging yang terstruktur. Di akhir episode, kalian akan mengotopsi sebuah skrip yang sengaja kita rusak — langkah demi langkah, sampai akar masalahnya tertangkap.
bash -n: Periksa Sintaks Tanpa Menjalankan Apa PunLangkah pertama debugging seharusnya selalu yang paling murah: pastikan skrip tidak memiliki kesalahan sintaks. BASH menyediakan mode noexec dengan -n — BASH membaca seluruh skrip, memeriksa sintaks, dan tanpa mengeksekusi satu baris pun. Ini seperti memeriksa cetak biru sebelum mulai membangun:
bash -n backup.sh
echo "exit: $?"Jika tidak ada output, sintaksnya valid dan exit code 0. Jika ada kesalahan, BASH mencetak lokasinya — dan di sini keunggulan -n terlihat: skrip yang masih berbahaya (misalnya berisi rm -rf) tidak akan dijalankan. Kalian bisa memeriksa skrip production dengan aman:
backup.sh: line 12: syntax error near unexpected token `done'
backup.sh: line 12: doneNote
bash -n adalah penyelamat saat kalian harus memeriksa skrip yang berjalan di server produksi dan tidak berani menjalankannya. Ia juga menemukan kesalahan klasik seperti kurung kurawal yang tidak seimbang, fi/done yang hilang, atau here-document yang tidak pernah ditutup — penyebab "misterius" dari banyak skrip yang tiba-tiba berhenti membaca input. Biasakan bash -n namaskrip.sh sebelum skrip kalian dianggap selesai.
bash -v: Lihat Baris yang Sedang DibacaJika -n hanya memeriksa, -v (verbose) mencetak setiap baris skrip sebelum mengeksekusinya — seperti guru yang membacakan soal keras-keras sebelum kalian menjawab. Ia menunjukkan urutan pembacaan BASH, yang berguna untuk mendeteksi struktur yang tidak kalian duga (misalnya blok yang ternyata tidak pernah tercapai karena logika salah):
bash -v deploy.shMode ini jarang dipakai sendirian — kombinasi paling umum adalah bash -vx untuk melihat baris yang dibaca sekaligus ekspansi yang terjadi. Tetapi kenali dulu keberadaannya; pada kasus-kasus aneh di mana skrip "berhenti membaca" di tengah, -v menunjukkan titik persis BASH berhenti meneruskan.
bash -x: Mode Trace — Melihat Ekspansi NyataIni adalah senjata utama debugging BASH. Mode xtrace (-x) mencetak setiap perintah yang akan dijalankan setelah ekspansi, diawali penanda +. Dengan ini, kalian melihat nilai sesungguhnya yang dijalankan BASH — bukan yang kalian tulis di kode:
#!/bin/bash
for file in /var/log/nginx/*.log; do
size=$(stat -c%s "$file")
echo "$file: $size bytes"
donebash -x size-check.sh+ for file in /var/log/nginx/*.log
+ size=$(stat -c%s /var/log/nginx/access.log)
++ stat -c%s /var/log/nginx/access.log
+ size=482739
+ echo /var/log/nginx/access.log: 482739 bytes
/var/log/nginx/access.log: 482739 bytesBacalah trace dari atas ke bawah: BASH mencetak + for file in ..., lalu + size=$(...), lalu ++ stat ... (dua tanda + menandakan subshell yang lebih dalam dari command substitution), lalu hasilnya. Pola inilah yang membuat -x begitu kuat: kalian melihat ekspansi variabel, glob, dan command substitution yang sebenarnya, sehingga jawaban "mengapa nilainya kosong?" bisa langsung terjawab.
Ada dua cara mengaktifkan trace: lewat baris perintah (bash -x skrip.sh) untuk debug sekali jalan, atau dari dalam skrip dengan set -x dan mematikannya dengan set +x. Versi dari dalam berguna untuk menelusuri sebagian skrip saja — area yang dicurigai:
#!/bin/bash
set -euo pipefail
echo "Fase 1: normal tanpa trace"
set -x
# Mulai dari sini, setiap perintah dicetak
for f in *.conf; do
cp "$f" "/backup/${f%.conf}.conf.bak"
done
set +x
echo "Fase 2: normal lagi"Ini pola yang sangat produktif: jangan membanjiri seluruh output skrip, cukup nyalakan trace di sekitar area yang sedang kalian selidiki, lalu matikan. Menyalakan set -x di seluruh skrip 500 baris akan menghasilkan 500 baris trace yang memusingkan — pinpoint adalah seni.
Tip
Ada varian praktis yang jarang diketahui: jika kalian tidak ingin trace bercampur dengan output normal (misalnya saat skrip mencetak laporan yang harus bersih), alihkan trace ke file dengan exec 2>trace.log lalu set -x — atau gunakan variabel khusus BASH_XTRACEFD: exec 4>trace.log; BASH_XTRACEFD=4; set -x. Trace tetap lengkap, laporan tetap murni. Kita akan membahas pengalihan stream lebih lanjut di episode integrasi.
Trace default bash -x hanya memberi penanda + di depan perintah. Untuk skrip besar, kalian tidak tahu baris mana di file yang menghasilkan perintah itu, atau dari fungsi mana ia berasal. Di sinilah PS4 berperan: ia adalah "template" penanda trace, dan bisa diisi dengan variabel konteks yang sangat berguna:
#!/bin/bash
export PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
set -x
greet() {
local nama="$1"
echo "Halo, $nama"
}
greet "Arman"+ /tmp/ps4.sh:6:greet: local nama=Arman
+ /tmp/ps4.sh:7:greet: echo Halo, Arman
Halo, ArmanLihat bagaimana setiap baris trace kini menyebutkan file (/tmp/ps4.sh), nomor baris (:6:), dan nama fungsi (greet). Untuk skrip dengan puluhan fungsi, jejak ini mengubah teka-teki "baris mana yang menjalankan ini?" menjadi jawaban langsung. Komponen PS4 yang paling berguna:
| Komponen | Makna |
|---|---|
${BASH_SOURCE} | Nama file yang sedang dieksekusi |
${LINENO} | Nomor baris saat ini |
${FUNCNAME[0]} | Nama fungsi yang sedang berjalan (indeks 0 = paling dalam) |
$0 | Nama skrip yang dipanggil |
Important
PS4 diekspor dengan export ke skrip yang dipanggil — tetapi jika kalian menjalankan bash -x skrip.sh langsung dari terminal, PS4 yang disetel di terminal ikut terbaca karena variabel environment diturunkan ke anak. Trik yang umum dipakai: setel export PS4='+ ${BASH_SOURCE}:${LINENO}: ' di ~/.bashrc, sehingga setiap sesi bash -x otomatis menghasilkan jejak yang kaya konteks tanpa menulis apa pun di skrip. Namun ingat jebakan di akhir episode: PS4 yang menyeret karakter khusus bisa membuat trace tidak terbaca.
trap ... DEBUG: Breakpoint ManualKadang trace penuh terlalu berisik, dan kalian hanya ingin berhenti memeriksa nilai di titik tertentu. BASH menyediakan signal khusus DEBUG yang memicu handler sebelum setiap perintah dijalankan. Dengan itu, kalian bisa membangun breakpoint sederhana:
#!/bin/bash
trap 'echo "[DEBUG] line ${LINENO}: nilai f = ${f:-kosong}"' DEBUG
for f in *.txt; do
[[ "$f" == *"tmp"* ]] && continue
echo "Proses $f"
done
trap - DEBUGSetiap perulangan, sebelum echo berikutnya dijalankan, trap mencetak status variabel f. Ini memberi kalian "pembacaan instrumen" tanpa harus menebak titik eksekusi. Kombinasi trap ... DEBUG dengan variabel $BASH_COMMAND (perintah yang akan dijalankan) bisa menghasilkan breakpoint yang meniru debugger sungguhan:
#!/bin/bash
trap 'printf "baris %s: %s\n" "$LINENO" "$BASH_COMMAND"' DEBUGWarning
trap ... DEBUG dieksekusi sebelum setiap perintah — termasuk perintah di dalam trap itu sendiri. Tanpa kehati-hatian, kalian bisa membuat rekursi atau banjir output. Selalu pasang breakpoint DEBUG hanya di sekitar area yang diselidiki, dan lepaskan dengan trap - DEBUG segera setelah selesai. Ini alat bedah, bukan alat untuk dipakai di seluruh tubuh pasien.
Kuasai alatnya, tapi yang lebih penting kuasai urutannya. Berikut alur yang saya rekomendasikan, dari termurah ke termahal:
shellcheck skrip.sh dulu. Sebagian besar bug ditemukan di sini: variabel tidak di-quote, [[ vs [, perintah yang bisa gagal di bawah set -e, dan banyak lagi. Ini "membaca keluhan pasien" sebelum operasi.bash -n — pastikan sintaks valid sebelum membuang waktu menelusuri.bash -x pada bagian yang dicurigai — lihat ekspansi yang sebenarnya.PS4 yang kaya konteks — jika skrip besar, aktifkan konteks file/baris/fungsi.trap ... DEBUG untuk memeriksa nilai variabel di titik spesifik.bash -x skrip.sh 2>debug.log) lalu periksa setelahnya.Sekarang mari terapkan semua teknik pada skrip yang sengaja kita rusak. Amati baik-baik — ada tiga bug yang mengintai:
#!/bin/bash
DIR=/var/log/nginx
TOTAL=0
for file in ${DIR}/*.log; do
lines=$(wc -l < "$file")
TOTAL=$(( TOTAL + lines ))
done
echo "Total baris log di $DIR: $TOTAL"Total baris log di /var/log/nginx: 0Bug 1 — glob yang ter-quote: for file in ${DIR}/*.log memang benar (tanpa kutip, globnya diekspansi). Jadi bukan itu. Mari jalankan bash -x:
+ DIR=/var/log/nginx
+ TOTAL=0
+ for file in /var/log/nginx/*.log
+ lines=4
+ TOTAL=4
+ for file in /var/log/nginx/*.log
+ lines=7
+ TOTAL=11
+ echo Total baris log di /var/log/nginx: 11Menarik — trace menunjukkan TOTAL=11, tetapi skrip aslinya mencetak 0. Berarti ada perbedaan antara apa yang kita pikir kita tulis dan apa yang sebenarnya dieksekusi. Ini petunjuk klasik bug pada baris yang kita percaya sudah benar — mari kita otopsi lebih dalam dengan PS4 untuk melihat nomor baris:
+ /tmp/buggy.sh:3:DIR=/var/log/nginx
+ /tmp/buggy.sh:4:TOTAL=0
+ /tmp/buggy.sh:6:for file in /var/log/nginx/*.log
+ /tmp/buggy.sh:7:lines=4
+ /tmp/buggy.sh:8:TOTAL=4
+ /tmp/buggy.sh:6:for file in /var/log/nginx/*.log
+ /tmp/buggy.sh:7:lines=7
+ /tmp/buggy.sh:8:TOTAL=11Trace menunjukkan skrip berjalan dengan benar dan menghasilkan 11. Kalau begitu, mengapa output aslinya 0? Jawabannya: kita tidak pernah benar-benar menjalankan skrip ini. Ketika kita "menjalankan" di atas, kita membayangkan, tetapi mari lihat apa yang terjadi jika glob tidak cocok dengan file apa pun — misalnya direktori log kosong. BASH secara default tidak mengembang glob yang tidak cocok: for file in /var/log/nginx/*.log menjadi satu nilai literal "/var/log/nginx/*.log", dan wc -l < "/var/log/nginx/*.log" gagal. Di versi skrip yang kita "jalankan" tadi output tetap 0 — itu adalah bug ketiga yang nyata: glob kosong.
+ DIR=/var/log/nginx
+ TOTAL=0
+ for file in /var/log/nginx/*.log
+ lines=
+ wc: /var/log/nginx/*.log: No such file or directory
+ TOTAL=0Di sinilah bash -x membuktikan nilainya: jejak menunjukkan wc mencoba membuka file bernama literal *.log, yang langsung mengarahkan ke akar masalah — glob yang tidak cocok tidak lenyap. Perbaikannya adalah shopt -s nullglob, yang membuat glob tanpa kecocokan menjadi daftar kosong:
#!/bin/bash
shopt -s nullglob
DIR=/var/log/nginx
TOTAL=0
for file in "$DIR"/*.log; do
lines=$(wc -l < "$file")
TOTAL=$(( TOTAL + lines ))
done
echo "Total baris log di $DIR: $TOTAL"Tip
Kebiasaan yang menyelamatkan waktu bertahun-tahun: jangan pernah mempercayai apa yang kalian bayangkan tentang skrip. bash -x adalah saksi mata yang tidak pernah berbohong — ia menunjukkan ekspansi yang terjadi, bukan yang kalian inginkan. Saat hasil skrip dan intuisi kalian bertabrakan, percayakan pada trace, bukan pada intuisi.
1. set -x mencemari output. Trace tercetak ke stderr secara default, jadi pipa skrip.sh | grep pattern tidak akan terkontaminasi. Tapi jika kalian menggabungkan stderr (2>&1) atau menangkap semuanya, jejak trace ikut tertangkap. Arahkan trace ke file (bash -x skrip.sh 2>debug.log) saat output harus bersih.
2. Output debug yang tidak di-quote. echo $file tanpa kutip di dalam handler debug akan kehilangan spasi dan memicu glob. Selalu quote: echo "$file". Ini ironis — skrip debug yang sendiri mengandung bug.
3. PS4 yang aneh membuat trace tidak terbaca. PS4 yang berisi karakter kontrol atau escape sequence (misalnya \e[31m) akan tercetak mentah sebagai teks aneh di beberapa terminal. Jika kalian memakai warna, pastikan menyertakan \e[0m penutup atau terima bahwa trace jadi berantakan. Simpan PS4 sederhana: + ${BASH_SOURCE}:${LINENO}: sudah cukup.
4. Lupa melepas trap DEBUG. Breakpoint yang dipasang di ~/.bashrc atau di awal skrip akan terus memburu setiap perintah, memperlambat eksekusi dan membanjiri output. trap - DEBUG adalah tombol mati; pakai.
5. Men-debug di environment yang berbeda dari produksi. Bug yang muncul di server tidak akan muncul di laptop jika versi BASH, locale, atau isi direktori berbeda. Selalu reproduksi di environment yang paling mirip produksi — dan jika ragu, bash --version dulu.
Di episode 20 ini kalian telah menjadi otopsi skrip yang sistematis. Kalian menguasai bash -n untuk memeriksa sintaks tanpa risiko, bash -v untuk melihat baris yang dibaca, bash -x untuk menelusuri ekspansi yang sebenarnya, PS4 untuk memperkaya jejak dengan file, baris, dan fungsi, serta trap ... DEBUG untuk breakpoint manual. Kalian juga mempraktikkan alur debug dari shellcheck hingga isolasi fungsi, dan membuktikan bahwa glob yang tidak cocok bisa menjadi akar kejahatan — sesuatu yang mustahil ditemukan tanpa saksi mata bash -x.
Poin kunci yang harus kalian bawa pulang:
bash -n murah dan aman — periksa sintaks sebelum apa pun.bash -x memperlihatkan ekspansi nyata; percayakan trace, bukan intuisi.PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: ' mengubah jejak menjadi peta.-n → -x → isolasi fungsi → log.shopt -s nullglob menyelesaikan salah satu bug terpopuler di dunia glob.Sekarang kalian bisa menemukan dan membuktikan penyebab bug. Ada satu lompatan terakhir menuju skrip yang benar-benar profesional: antarmuka pengguna. Skrip yang dipakai orang lain — bukan hanya oleh kalian — butuh flag, opsi, dan pesan bantuan yang jelas. Di episode 21 selanjutnya kita akan membangun CLI Professional dengan getopts: ./backup.sh -d /var/www -v -h, fungsi usage(), penanganan opsi yang butuh nilai, dan seluruh pola yang membuat skrip kalian terasa seperti alat yang dipoles, bukan sekadar file teks. Sampai jumpa!