Belajar BASH Scripting - Debugging Techniques
Episode 20 of 27

Belajar BASH Scripting - Debugging Techniques

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.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

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.

Pembahasan Utama

bash -n: Periksa Sintaks Tanpa Menjalankan Apa Pun

Langkah 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:

Pemeriksaan sintaks tanpa eksekusi
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:

Contoh output saat ada kesalahan sintaks
backup.sh: line 12: syntax error near unexpected token `done'
backup.sh: line 12: done

Note

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 Dibaca

Jika -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):

Menjalankan skrip dengan mode verbose
bash -v deploy.sh

Mode 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 Nyata

Ini 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:

Skrip dengan bug halus
#!/bin/bash
 
for file in /var/log/nginx/*.log; do
    size=$(stat -c%s "$file")
    echo "$file: $size bytes"
done
Menelusuri eksekusinya dengan bash -x
bash -x size-check.sh
Output trace (contoh)
+ 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 bytes

Bacalah 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:

Trace hanya pada bagian tertentu
#!/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.

PS4: Membuat Jejak Trace Berbicara

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:

PS4 dengan konteks lengkap
#!/bin/bash
 
export PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
set -x
greet() {
    local nama="$1"
    echo "Halo, $nama"
}
greet "Arman"
Output trace dengan PS4
+ /tmp/ps4.sh:6:greet: local nama=Arman
+ /tmp/ps4.sh:7:greet: echo Halo, Arman
Halo, Arman

Lihat 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:

KomponenMakna
${BASH_SOURCE}Nama file yang sedang dieksekusi
${LINENO}Nomor baris saat ini
${FUNCNAME[0]}Nama fungsi yang sedang berjalan (indeks 0 = paling dalam)
$0Nama 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 Manual

Kadang 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:

Breakpoint dengan trap DEBUG
#!/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 - DEBUG

Setiap 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:

Breakpoint menampilkan perintah berikutnya
#!/bin/bash
 
trap 'printf "baris %s: %s\n" "$LINENO" "$BASH_COMMAND"' DEBUG

Warning

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.

Strategi Debugging: Urutan yang Terbukti

Kuasai alatnya, tapi yang lebih penting kuasai urutannya. Berikut alur yang saya rekomendasikan, dari termurah ke termahal:

  1. 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.
  2. bash -n — pastikan sintaks valid sebelum membuang waktu menelusuri.
  3. bash -x pada bagian yang dicurigai — lihat ekspansi yang sebenarnya.
  4. PS4 yang kaya konteks — jika skrip besar, aktifkan konteks file/baris/fungsi.
  5. Isolasi fungsi — panggil satu fungsi sendirian dengan input uji, bukan menjalankan seluruh skrip.
  6. trap ... DEBUG untuk memeriksa nilai variabel di titik spesifik.
  7. Log ke file — untuk bug yang butuh waktu lama atau berjalan di cron, arahkan output debug ke file log (bash -x skrip.sh 2>debug.log) lalu periksa setelahnya.

Praktik: Otopsi Skrip yang Sengaja Rusak

Sekarang mari terapkan semua teknik pada skrip yang sengaja kita rusak. Amati baik-baik — ada tiga bug yang mengintai:

buggy.sh — skrip yang bermasalah
#!/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"
Output yang mencurigakan
Total baris log di /var/log/nginx: 0

Bug 1 — glob yang ter-quote: for file in ${DIR}/*.log memang benar (tanpa kutip, globnya diekspansi). Jadi bukan itu. Mari jalankan bash -x:

Trace bash -x buggy.sh
+ 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: 11

Menarik — 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:

Trace dengan PS4 memperlihatkan lokasi asli
+ /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=11

Trace 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.

Trace saat direktori 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=0

Di 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:

buggy.sh — diperbaiki
#!/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.

Jebakan Klasik (Common Pitfalls)

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.

Penutup

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.
  • Debug urut: shellcheck → -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!

Belajar BASH Scripting - Debugging Techniques | Belajar BASH Scripting