Belajar BASH Scripting - Static Analysis & Automated Testing (ShellCheck & Bats)
Episode 25 of 27

Belajar BASH Scripting - Static Analysis & Automated Testing (ShellCheck & Bats)

Pastikan skrip BASH aman sebelum dipakai produksi: analisis statis dengan ShellCheck untuk menangkap bug umum seperti SC2086 dan SC2164, lalu pengujian otomatis dengan Bats-core. Disertai praktik menulis test untuk fungsi utilitas dan jebakan dalam pengujian shell.

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

Pendahuluan

Setelah di episode 24 sebelumnya kita membahas logging, colorizing output, dan terminal UX — bagaimana membuat skrip yang rapi, tercatat, dan nyaman dipakai — pada episode kali ini kita membahas hal yang sering dianggap "tidak sempat" tapi sebenarnya paling menentukan: menjamin kualitas skrip sebelum ia benar-benar dipakai.

Inilah ironi dunia BASH scripting: tidak ada bahasa lain yang dipakai sebanyak shell, tapi sedikit sekali yang diuji. Kalian tidak akan mengirim aplikasi Python tanpa unit test, tetapi banyak orang men-deploy skrip BASH yang belum pernah diverifikasi selain "ya, kemarin sih jalan". Padahal skrip BASH rawan error yang tidak terlihat: variabel yang salah ketik, tanda kutip yang hilang, direktori yang tidak ada, path yang tidak ter-quote. Error semacam ini biasanya baru meledak di tengah malam, di server produksi, ketika tidak ada yang melihat.

Dua alat akan menjadi pengaman kalian. ShellCheck adalah static analyzer: ia membaca skrip kalian tanpa menjalankannya dan menunjuk ke ratusan pola bug yang umum — seperti pemeriksa naskah yang menandai kalimat ambigu sebelum dicetak. Bats-core adalah framework pengujian: kalian menulis ekspektasi ("fungsi ini harus mengembalikan 0 saat dijalankan sebagai root"), dan Bats menjalankannya untuk kalian setiap kali skrip berubah — seperti jaringan pengaman yang menangkap regresi.

Analoginya: ShellCheck adalah spell checker yang bekerja saat kalian menulis, sedangkan Bats adalah ujian yang memastikan perilaku tetap benar setelah kalian mengubah sesuatu. Menggabungkan keduanya mengubah skrip BASH kalian dari "kode yang mudah rusak" menjadi "kode yang bisa dipercaya" — dan kepercayaan inilah yang menjadi syarat utama sebelum skrip masuk ke produksi.

Pada episode ini kita akan membedah ShellCheck beserta aturan-aturannya, lalu Bats-core beserta struktur pengujiannya, mempraktikkannya pada skrip utilitas nyata, dan menutup dengan jebakan yang paling sering ditemui saat menguji shell.

Pembahasan Utama

ShellCheck: Pemeriksa Statis yang Tidak Pernah Lelah

ShellCheck adalah linter untuk skrip shell yang menangkap ratusan kelas bug sebelum skrip dieksekusi. Ia tidak menjalankan skrip — ia menganalisis pola penulisan dan membandingkannya dengan perilaku bash yang diketahui. Instalasi di Debian/Ubuntu:

Instal dan jalankan ShellCheck
apt install shellcheck        # Debian/Ubuntu
shellcheck deploy.sh          # jalankan pada skrip kalian

Setiap temuan punya kode unik (misal SC2086) dan empat tingkat keparahan: error (kemungkinan besar salah), warning (kemungkinan salah), info (tidak yakin, periksa), dan style (perbaikan kosmetik). Semakin tinggi keparahan, semakin besar kemungkinan bug sungguhan.

Beberapa kode yang paling sering muncul dan paling sering merusak produksi:

KodeMaksudContoh
SC2086Variabel tidak di-quote (word splitting)rm $filerm "$file"
SC2164cd tanpa cek kegagalancd "$dir" → `cd "$dir"
SC2034Variabel dideklarasikan tapi tidak dipakaitypo nama variabel
SC2001`echo ...sed` yang bisa diganti parameter expansion
SC2162read tanpa -r (backslash ditafsirkan)read lineread -r line

Lihat bagaimana SC2086 bekerja pada contoh nyata — ini bug paling klasik di dunia shell:

SC2086: variabel tanpa tanda kutip
rm -rf $BACKUP_DIR/*.tmp
rm -rf "$BACKUP_DIR"/*.tmp

Tanpa tanda kutip, jika $BACKUP_DIR berisi spasi (misal /var/backup harian), perintahnya terpecah menjadi dua argumen — dan rm -rf pada path yang salah adalah kecelakaan paling ditakuti admin. ShellCheck menandainya dalam sekejap, tanpa harus menunggu skrip dieksekusi dengan data nyata.

Untuk melihat seperti apa temuan ShellCheck dalam praktik, jalankan pada skrip yang sengaja ditulis buruk — perhatikan bagaimana satu perintah menghasilkan beberapa temuan sekaligus:

Membuat skrip contoh dengan beberapa bug
cat > demo.sh <<'EOF'
#!/usr/bin/env bash
cd /tmp
echo $1
for i in $(ls); do
    echo "file: $i"
done
EOF
shellcheck demo.sh
Contoh output ShellCheck dengan tingkatan keparahan
In demo.sh line 2:
cd /tmp
^-- SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
 
In demo.sh line 3:
echo $1
     ^-- SC2086 (info): Double quote to prevent globbing and word splitting.
 
In demo.sh line 4:
for i in $(ls);
        ^-- SC2045 (error): Iterating over ls output is fragile.
Setiap temuan dilabeli level error/warning/info/style

Perhatikan tiga temuan berbeda pada tiga level berbeda: SC2045 sebagai error (mengiterasi output ls berbahaya), SC2164 sebagai warning (kegagalan cd bisa membuat skrip berlanjut di direktori yang salah), dan SC2086 sebagai info (quoting hilang). Dengan satu perintah, ShellCheck menemukan tiga bug yang masing-masing bisa meledak di produksi.

Terkadang kalian tahu lebih baik — misalnya variabel yang memang sengaja dibiarkan tidak terpakai, atau perilaku yang disengaja di lingkungan tertentu. Untuk kasus ini, ShellCheck menyediakan pengecualian dengan komentar disable:

Nonaktifkan peringatan spesifik dengan alasan
# shellcheck disable=SC2034
readonly DEFAULT_REGION="ap-southeast-1"

Note

Pengecualian adalah utang teknis. Setiap # shellcheck disable=SC... sebaiknya disertai komentar singkat mengapa — misal # SC2034: dipakai oleh library yang di-source saat runtime. Sekelompok disable tanpa alasan sama bahayanya dengan menonaktifkan semua alarm kebakaran: sepertinya mempermudah, padahal menghilangkan perlindungan.

Kekuatan sesungguhnya ShellCheck muncul saat diintegrasikan ke alur kerja. Di editor, ekstensi VS Code atau vim dengan plugin cocok menampilkan temuan langsung saat mengetik. Di CI/CD, cukup tambahkan satu langkah pipeline sehingga pull request yang mengandung bug tidak pernah ter-merge:

LinuxContoh langkah CI dengan ShellCheck
name: Lint shell scripts
on: [push, pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: apt-get update && apt-get install -y shellcheck
      - run: shellcheck scripts/*.sh

Dengan konfigurasi ini, setiap kali seseorang melakukan push skrip dengan bug, pipeline gagal sebelum skrip sempat menjalankan perintah berbahaya apa pun.

Bats-core: Framework Pengujian untuk Shell

ShellCheck memastikan sintaks dan pola aman, tapi tidak bisa memastikan perilaku benar. Untuk itu kita butuh pengujian otomatis — dan untuk shell, standar de facto-nya adalah Bats-core (Bash Automated Testing System).

Mengapa harus framework? BASH tidak punya mekanisme test bawaan, dan menulis pengujian manual dengan if berantai adalah resep chaos. Bats memberi tiga hal: struktur @test, fungsi bantuan run/$status/$output, dan pelaporan hasil yang rapi. Instalasi:

Instal Bats-core
apt install bats            # Debian/Ubuntu (bats-core)
git clone https://github.com/bats-core/bats-core /opt/bats
/opt/bats/bin/bats --version

Struktur file .bats sangat deklaratif. Setiap @test mendeklarasikan nama, lalu berisi tiga elemen inti: run <perintah> untuk menjalankan sesuatu dan menangkap hasilnya, $status untuk kode keluar, dan $output untuk teks yang dihasilkan:

Struktur dasar file .bats
#!/usr/bin/env bats
 
@test "perintah echo mengembalikan status 0" {
    run echo "halo dunia"
    [ "$status" -eq 0 ]
    [ "$output" = "halo dunia" ]
}
 
@test "grep menemukan pola yang ada" {
    run grep "error" log-sample.txt
    [ "$status" -eq 0 ]
    [ "$output" != "" ]
}

Perhatikan pola yang berulang: run selalu muncul sebelum asersi. Bats menangkap stdout dan stderr ke $output, dan kode keluar ke $status. Untuk mencocokkan sebagian teks, gunakan glob:

Asersi substring dan kombinasi
@test "pesan error mengandung kata kunci" {
    run myscript.sh --invalid-flag
    [ "$status" -ne 0 ]
    [[ "$output" == *"unknown option"* ]]
}

Gaya [[ "$output" == *"..."* ]] menguji apakah output mengandung substring — jauh lebih fleksibel daripada persamaan penuh.

Untuk berbagi fungsi bantu antar file test, gunakan load. Buat file helpers.bash berisi fungsi yang men-setup environment, lalu muat di setiap file test:

Memuat helper ke dalam test
load helpers
 
@test "skrip tanpa argumen menampilkan usage" {
    run myscript.sh
    [[ "$output" == *"Usage:"* ]]
}

Bats juga mendukung setup() dan teardown() — fungsi yang dijalankan sebelum dan sesudah setiap test, berguna untuk membuat file sementara atau membersihkan artefak.

Praktik: Menulis Test untuk Fungsi Utilitas

Mari terapkan pada skenario nyata. Anggap kita punya lib/utils.sh dengan dua fungsi: is_root (mengecek apakah dijalankan sebagai root) dan is_number (mengecek apakah argumen berupa angka). Keduanya adalah kandidat sempurna untuk diuji:

/opt/scripts/lib/utils.sh
is_root() {
    [ "$(id -u)" -eq 0 ]
}
 
is_number() {
    case "$1" in
        ''|*[!0-9]*) return 1 ;;
        *) return 0 ;;
    esac
}

Sekarang tulis pengujiannya di utils.bats. Kita perlu source fungsi-fungsi tersebut — karena mereka bukan executable, kita muat dengan source di dalam setiap test:

/opt/scripts/test/utils.bats
#!/usr/bin/env bats
 
load helpers
 
setup() {
    source "$BATS_TEST_DIRNAME/../lib/utils.sh"
}
 
@test "is_number menerima angka" {
    run is_number "2026"
    [ "$status" -eq 0 ]
}
 
@test "is_number menolak teks campuran" {
    run is_number "12abc"
    [ "$status" -ne 0 ]
}
 
@test "is_number menolak string kosong" {
    run is_number ""
    [ "$status" -ne 0 ]
}
 
@test "is_root mengembalikan status sesuai id -u" {
    run is_root
    if [ "$(id -u)" -eq 0 ]; then
        [ "$status" -eq 0 ]
    else
        [ "$status" -ne 0 ]
    fi
}

Jalankan dengan perintah bats:

Menjalankan seluruh suite test
bats test/utils.bats
Output Bats yang menunjuk test gagal
 ✓ is_number menerima angka
 ✓ is_number menolak teks campuran
 ✓ is_number menolak string kosong
 ✓ is_root mengembalikan status sesuai id -u
 
 4 tests, 0 failures
Simbol ✓ untuk sukses, ✗ untuk gagal beserta detail

Perhatikan betapa cepatnya pengujian ini memberi kepercayaan: setiap kali kalian mengubah is_number (misalnya untuk mendukung angka negatif), satu perintah bats langsung memberi tahu jika ada perilaku lama yang rusak. Inilah jaring pengaman yang mencegah regresi masuk ke produksi.

Tip

Satu pengujian, satu perilaku. Pecah setiap perilaku menjadi @test sendiri, bukan menggabungkan banyak asersi dalam satu test. Ketika suite gagal, nama test yang jelas langsung menunjuk ke perilaku yang rusak — menghemat waktu debugging yang sangat berharga dibanding harus membaca seratus baris output.

Seperti ShellCheck, Bats juga paling berharga saat dipasang di pipeline. Di GitHub Actions, cukup satu langkah untuk menjalankan seluruh suite setiap kali kode berubah:

LinuxJalankan Bats di CI setelah ShellCheck
      - name: Run Bats tests
        run: |
          apt-get install -y bats
          bats test/

Sekarang setiap pull request yang mengubah fungsi is_number harus membuktikan bahwa semua perilaku lamanya masih utuh — regresi terdeteksi dalam hitungan menit, bukan berminggu-minggu kemudian di produksi. Inilah yang dimaksud automated testing: mesin yang mengingatkan kalian tentang hal yang kalian lupa.

Common Pitfalls: Jebakan Static Analysis & Pengujian

1. ShellCheck melaporkan "false positive". Terkadang ShellCheck benar-benar salah paham konteks — misalnya variabel yang diisi oleh library yang di-source, atau pola ekspansi yang disengaja. Solusinya bukan membungkam seluruh file, tapi disable per baris dengan komentar alasan (lihat sebelumnya). Periksa dulu apakah temuan itu benar, baru nonaktifkan.

2. run tidak menangkap output dengan benar. Kekeliruan paling umum: menguji $output tanpa memanggil run terlebih dahulu. $output hanya terisi setelah run. Ingat urutan baku: run perintah → cek $status → cek $output. Menguji variabel yang belum terisi menghasilkan asersi yang selalu gagal (atau lebih buruk, selalu lolos) — hasil yang menyesatkan.

3. Menguji skrip interaktif. Skrip yang memanggil read atau select akan menggantung di dalam Bats, karena tidak ada terminal yang merespons. Solusinya bukan memaksa test, melainkan refactor: pisahkan logika menjadi fungsi murni yang menerima input sebagai parameter, dan simpan bagian interaktif di lapisan tipis. Ini pola yang sama dengan dependency injection di bahasa pemrograman — dan justru membuat skrip kalian lebih mudah diuji dan lebih mudah dibaca.

4. Lupa chmod +x atau tidak men-source. Test yang memanggil fungsi dengan run langsung dari file .sh non-executable akan gagal misterius. Pastikan fungsi di-source di setup(), dan file script yang dijalankan memiliki izin eksekusi.

5. Test yang "selalu lolos". [ "$status" -eq 0 ] tanpa pernah memanggil run adalah bom waktu: $status kosong, perbandingan -eq pada string kosong error, dan Bats melaporkan test gagal — atau lebih buruk, [[ ]] yang salah justru lolos. Selalu verifikasi bahwa test kalian bisa gagal: jalankan sekali dengan input yang sengaja salah dan pastikan suite benar-benar jatuh.

Penutup

Pada episode 25 ini, kalian telah menambahkan lapisan kualitas profesional pada skrip BASH. Dengan ShellCheck, kita menangkap bug statis sejak awal: menginstal linter, membaca temuan dengan kode seperti SC2086 (variabel tanpa tanda kutip), SC2164 (cd tanpa cek), dan SC2034 (variabel tak terpakai), menonaktifkan temuan palsu dengan # shellcheck disable=... yang beralasan, dan mengintegrasikannya ke editor serta pipeline CI. Dengan Bats-core, kita membangun pengujian otomatis: struktur file .bats dengan @test, pola run$status$output, asersi substring dengan [[ == *...* ]], load untuk helper, hingga setup()/teardown(). Kita mempraktikkannya dengan menguji fungsi is_root dan is_number, dan menutup dengan jebakan yang paling sering membuat pengujian shell menyesatkan.

Poin kunci yang perlu kalian bawa:

  • ShellCheck menangkap pola yang salah; Bats menangkap perilaku yang salah — keduanya saling melengkapi.
  • Jangan pernah men-deploy skrip yang tidak lolos ShellCheck tanpa alasan yang terdokumentasi.
  • Pola baku Bats: run → cek $status → cek $output; jangan menguji variabel yang belum terisi.
  • Refactor skrip interaktif menjadi fungsi murni agar mudah diuji.
  • Pastikan setiap test mampu gagal — test yang selalu lolos tidak memberi informasi.

Kalian kini memiliki seluruh peralatan: sintaks yang solid, kontrol alur, integrasi API, logging, dan jaminan kualitas. Tinggal satu episode lagi. Di episode 26 — episode penutup series ini — kita akan merakit semuanya menjadi skenario praktis production-grade: skrip backup & retensi otomatis serta health check & auto-healing yang lengkap, plus rekap seluruh perjalanan kalian dari episode 0 hingga 26. Sampai di sana!