Secara bawaan, script bash jalan terus walaupun ada perintah yang gagal, dan menganggap variabel yang salah ketik sebagai teks kosong. Defensive scripting artinya menyalakan pengaman supaya script berhenti di masalah pertama, lalu selalu bersih-bersih sebelum keluar. Bayangkan dapur restoran: koki berhenti memasak begitu menemukan bahan basi (itu set -e), dan dapur selalu dibersihkan di akhir shift, entah malam itu lancar, ada insiden, atau tutup lebih awal (itu trap ... EXIT).
🧪 Tebakan cepat: script
backup.shdi bawah dijalankan tanpa pengaman apa pun. Dua langkahnya gagal. Script itu tetap mencetak3. selesai? Dan exit code-nya 0 atau 1? Tebak dulu, jawabannya ada di bagian "Tanpa pengaman: jalan terus".
Script contohnya
#!/usr/bin/env bash
set -euo pipefail
TMP=$(mktemp -d)
trap 'echo "cleanup: hapus $TMP"; rm -rf "$TMP"' EXIT
trap 'echo "ERROR di baris $LINENO, exit $?" >&2' ERR
echo "1. hitung error di log"
grep ERROR app.log | wc -l
echo "2. salin database"
cp data.db "$BACKUP_DIRR/"
echo "3. selesai"
Ada tiga masalah yang sengaja ditanam: app.log gak ada, BACKUP_DIRR salah ketik (harusnya BACKUP_DIR), dan user biasa gak boleh menulis ke folder /. Baris 2, 4, dan 5 adalah pengamannya. Kita lihat dulu apa yang terjadi tanpa ketiganya.
Exit code adalah angka yang ditinggalkan tiap perintah saat selesai: 0 artinya berhasil, selain 0 artinya gagal. echo $? menampilkan exit code perintah terakhir.
Tanpa pengaman: jalan terus
Baris 2, 4, dan 5 dihapus:
$ bash backup.sh; echo "exit: $?"
1. hitung error di log
grep: app.log: No such file or directory
0
2. salin database
cp: cannot create regular file '/data.db': Permission denied
3. selesai
exit: 0
- Baris 8:
grepgagal, tapi exit code sebuah pipe diambil dari perintah terakhir, yaituwc, yang sukses menghitung 0 baris. Kegagalannya tertelan. - Baris 10:
$BACKUP_DIRRgak pernah di-set, jadi bash menggantinya dengan teks kosong. Perintahnya jadicp data.db /, dan ditolak. - Baris 11: script tetap mencetak
3. selesai, dan exit code-nya0. Itu jawaban tebakannya: dari luar, backup ini kelihatan sukses padahal gak ada yang tersalin. Folder temp darimktempjuga tertinggal.
Tiga pengaman: set -euo pipefail
set -euo pipefail sama dengan set -e -u -o pipefail. Ini pengaruh tiap hurufnya ke backup.sh:
| Pengaman | Artinya | Yang terjadi di backup.sh |
|---|---|---|
-e (exit on error) |
berhenti begitu ada perintah yang gagal | berhenti di baris 10 (cp), exit 1 |
-o pipefail |
pipe dianggap gagal kalau salah satu bagiannya gagal | baris 8 jadi gagal dengan exit 2 dari grep |
-u (undefined fails) |
variabel yang belum di-set jadi error, bukan teks kosong | baris 10: backup.sh: line 10: BACKUP_DIRR: unbound variable |
-x (xtrace) |
cetak tiap perintah dengan awalan + sebelum dijalankan |
untuk debug, gak menghentikan apa pun |
Dengan baris 2 saja (tanpa trap), script berhenti di masalah pertama:
$ bash backup.sh; echo "exit: $?"
1. hitung error di log
grep: app.log: No such file or directory
0
exit: 2
0 tetap tercetak, karena kedua sisi pipe jalan bersamaan. Bedanya, pipefail membuat pipe-nya gagal, dan -e menghentikan script di situ. Tanpa pipefail, -e melewatkan baris 8 dan baru berhenti di baris 10.
-e sengaja gak berlaku di tempat yang memang mengetes berhasil atau gagal: kondisi if dan while, perintah di sebelah kiri && atau ||, dan perintah yang dibalik dengan !. Jadi if grep -q ERROR app.log; then tetap aman.
Nilai cadangan: ${VAR:-default}
Dengan -u, variabel yang boleh kosong perlu nilai cadangan:
PORT="${PORT:-3000}" # PORT kosong atau belum di-set: pakai 3000
: "${DB_HOST:?DB_HOST wajib di-set}" # berhenti dengan pesan ini kalau kosong
Isi PORT |
${PORT:-3000} |
|---|---|
| belum di-set | 3000 |
"" (kosong) |
3000 |
8080 |
8080 |
Titik duanya penting: ${PORT:-3000} memakai cadangan untuk yang belum di-set dan yang kosong. Tanpa titik dua, ${PORT-3000}, teks kosong tetap dipakai apa adanya.
trap: bersih-bersih yang pasti jalan
trap 'perintah' EXIT menjalankan perintah itu saat script berakhir, apa pun sebabnya: selesai normal, dihentikan -e atau -u, Ctrl+C, atau kill. Pengecualiannya cuma kill -9, yang gak bisa ditangkap. trap '...' ERR jalan tiap kali ada perintah yang gagal. ERR gak butuh -e, tapi juga gak jalan untuk error -u, karena bash langsung keluar.
Dengan semua pengaman menyala:
$ bash backup.sh; echo "exit: $?"
1. hitung error di log
grep: app.log: No such file or directory
0
ERROR di baris 8, exit 2
cleanup: hapus /tmp/tmp.k3J9qL2Abx
exit: 2
Urutannya: perintah gagal, trap ERR melapor, -e menghentikan script, trap EXIT bersih-bersih. Exit code-nya tetap 2, trap gak mengubahnya.
Satu sinyal cuma punya satu trap: trap ... EXIT yang kedua menggantikan yang pertama. Kalau ada beberapa hal yang harus dibereskan, kumpulkan dalam satu function, dan bereskan dengan urutan terbalik dari saat menyiapkannya. Yang terakhir dibuat, dibereskan duluan:
cleanup() {
rm -f "$LOCK" # dibuat terakhir, dibereskan duluan
rm -rf "$TMP" # dibuat pertama, dibereskan terakhir
}
trap cleanup EXIT
⚠️ Jebakan yang sering ketemu
set -etanpapipefail. Kegagalan di tengah pipe tetap lolos, karena yang dilihat cuma perintah terakhir.- Trap dipasang terlambat. Kalau script gagal sebelum baris
trap, gak ada yang bersih-bersih. Pasang trap tepat setelah folder temp dibuat. - Dua
trap ... EXIT. Yang kedua menghapus yang pertama. Satukan dalam satu functioncleanup. -udengan variabel opsional.$VERBOSEyang memang boleh gak ada bikin script berhenti. Tulis${VERBOSE:-}atau beri cadangan.
TL;DR: Pasang set -euo pipefail di baris kedua tiap script: -e berhenti di perintah gagal pertama, -u menolak variabel yang belum di-set, -o pipefail membuat pipe gagal kalau salah satu bagiannya gagal, dan -x mencetak tiap perintah untuk debug. -e gak berlaku di kondisi if/while dan di kiri &&/||. Untuk variabel opsional pakai ${VAR:-default}: cadangannya dipakai saat variabel belum di-set atau kosong. trap '...' EXIT pasti jalan saat script berakhir (sukses, gagal, Ctrl+C), trap '...' ERR jalan tiap ada perintah gagal. Satu sinyal satu trap, jadi kumpulkan bersih-bersih dalam satu function dan bereskan dengan urutan terbalik.