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.

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.
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:
apt install shellcheck # Debian/Ubuntu
shellcheck deploy.sh # jalankan pada skrip kalianSetiap 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:
| Kode | Maksud | Contoh |
|---|---|---|
SC2086 | Variabel tidak di-quote (word splitting) | rm $file → rm "$file" |
SC2164 | cd tanpa cek kegagalan | cd "$dir" → `cd "$dir" |
SC2034 | Variabel dideklarasikan tapi tidak dipakai | typo nama variabel |
SC2001 | `echo ... | sed` yang bisa diganti parameter expansion |
SC2162 | read tanpa -r (backslash ditafsirkan) | read line → read -r line |
Lihat bagaimana SC2086 bekerja pada contoh nyata — ini bug paling klasik di dunia shell:
rm -rf $BACKUP_DIR/*.tmp
rm -rf "$BACKUP_DIR"/*.tmpTanpa 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:
cat > demo.sh <<'EOF'
#!/usr/bin/env bash
cd /tmp
echo $1
for i in $(ls); do
echo "file: $i"
done
EOF
shellcheck demo.shIn 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.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:
# 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:
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/*.shDengan konfigurasi ini, setiap kali seseorang melakukan push skrip dengan bug, pipeline gagal sebelum skrip sempat menjalankan perintah berbahaya apa pun.
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:
apt install bats # Debian/Ubuntu (bats-core)
git clone https://github.com/bats-core/bats-core /opt/bats
/opt/bats/bin/bats --versionStruktur 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:
#!/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:
@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:
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.
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:
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:
#!/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:
bats test/utils.bats ✓ is_number menerima angka
✓ is_number menolak teks campuran
✓ is_number menolak string kosong
✓ is_root mengembalikan status sesuai id -u
4 tests, 0 failuresPerhatikan 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:
- 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.
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.
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:
run → cek $status → cek $output; jangan menguji variabel yang belum terisi.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!