Penanganan error

Penanganan error dengan trap, pembersihan sumber daya, dan pola logging yang teratur.

Daftar isi

Skrip otomasi hampir pasti menemui keadaan gagal, misalnya berkas sumber yang tidak ada atau direktori kerja yang tidak dapat dibuat. Topik ini membahas tiga pola penanganan error pada skrip bash, yaitu pembersihan sumber daya dengan trap (mekanisme yang menjadwalkan perintah untuk dijalankan ketika shell menerima sinyal atau peristiwa tertentu), pelaporan lokasi kegagalan dengan trap ERR, dan penyusunan log (catatan berjalan atas kejadian dalam program) yang teratur. Contohnya berupa skrip arsip cadangan yang membuat direktori kerja sementara, mengemas isinya, lalu menghapusnya kembali. Strict mode tetap diaktifkan pada setiap skrip contoh, sehingga kegagalan langsung menghentikan skrip dan trap tetap berkesempatan membersihkan.

Membersihkan sumber daya dengan trap#

Banyak skrip membuat sumber daya sementara sebelum bekerja, misalnya direktori kerja atau berkas hasil antara. Sumber daya itu tertinggal apabila skrip berhenti di tengah jalan, dan sisa semacam ini menumpuk menjadi masalah tersendiri pada mesin yang menjalankan otomasi setiap hari. Perintah builtin trap menjawab masalah tersebut dengan menjadwalkan sebuah fungsi untuk dijalankan pada peristiwa tertentu. Peristiwa EXIT terjadi setiap kali skrip berakhir, baik karena baris terakhirnya selesai, karena exit, maupun karena kegagalan yang dipotong oleh strict mode. Pola trap bersih EXIT dengan demikian menjamin fungsi pembersihan berjalan pada semua jalur akhir.

bash skrip-arsip.sh
# tidak-dijalankan
# Arsip cadangan dengan pembersihan terjamin melalui trap EXIT.

set -euo pipefail

log() {
  printf '%s\n' "[INFO] $*"
}

error_log() {
  printf '%s\n' "[ERROR] $*" >&2
}

kerja="kerja-sementara"

bersih() {
  rm -rf "$kerja"
  log "pembersihan selesai"
}
trap bersih EXIT

sumber="$1"

if [ ! -f "$sumber" ]; then
  error_log "berkas sumber tidak ditemukan: $sumber"
  exit 1
fi

log "menyiapkan direktori kerja"
mkdir -p "$kerja"
log "menyalin $sumber"
cp "$sumber" "$kerja/"
log "mengemas arsip-cadangan.tar.gz"
tar -czf arsip-cadangan.tar.gz "$kerja"
log "arsip selesai"

Skrip ini memakai dua fungsi kecil untuk logging (pencatatan kejadian ke dalam log), yaitu log untuk kejadian normal dan error_log untuk pesan gagal yang dikirim ke aliran error. Fungsi bersih menghapus direktori kerja-sementara, dan pemasangannya sebagai trap EXIT dilakukan segera setelah variabel kerja ditetapkan. Baris pertama skrip bertanda # tidak-dijalankan karena mengaktifkan strict mode, sehingga skrip dijalankan melalui bash skrip-arsip.sh dan tidak disalin langsung ke sesi terminal.

Jalur sukses dan jalur gagal#

Jalankan skrip dengan berkas sumber yang benar-benar ada. Perintah berikut menyiapkan berkas catatan, lalu memanggil skrip arsip.

bash
# siapkan berkas catatan.txt lalu buat arsip cadangan
printf 'catatan penting\nbaris kedua\n' > catatan.txt
bash skrip-arsip.sh catatan.txt
text Keluaran
[INFO] menyiapkan direktori kerja
[INFO] menyalin catatan.txt
[INFO] mengemas arsip-cadangan.tar.gz
[INFO] arsip selesai
[INFO] pembersihan selesai

Lima baris keluaran memuat empat catatan langkah dan satu catatan pembersihan. Baris [INFO] pembersihan selesai muncul paling akhir karena trap EXIT dijalankan setelah tubuh skrip selesai, termasuk pada jalur sukses sekalipun. Direktori kerja-sementara dengan demikian tidak pernah tertinggal, dan berkas arsip-cadangan.tar.gz menjadi hasil akhirnya.

bash
# panggil dengan berkas yang tidak ada
bash skrip-arsip.sh tidak-ada.txt
echo "kode keluar: $?"
text Keluaran
[ERROR] berkas sumber tidak ditemukan: tidak-ada.txt
[INFO] pembersihan selesai
kode keluar: 1

Pada jalur gagal, pesan [ERROR] mencatat penyebabnya, fungsi pembersihan tetap berjalan, dan kode keluar 1 sampai ke pemanggil karena exit 1 dijalankan sebelum trap. Perlu diketahui bahwa trap EXIT tidak mengubah kode keluar skrip selama fungsi pembersihannya tidak memanggil exit dengan nilai lain. Inilah alasan fungsi pembersihan dibuat sederhana, yaitu hanya menghapus dan mencatat, sehingga jalur keluarnya dapat diprediksi.

Melaporkan lokasi kegagalan dengan trap ERR#

Pembersihan menjawab apa yang harus dilakukan setelah kegagalan, sedangkan trap ERR menjawab di mana kegagalan itu terjadi. Peristiwa ERR terjadi setiap kali perintah sederhana berakhir dengan kode keluar bukan nol, dan trapnya dapat memakai dua nilai penting, yaitu $LINENO yang menyimpan nomor baris perintah yang gagal serta $? yang menyimpan kode keluarnya. Keduanya dikirim sebagai argumen fungsi pelapor sehingga teks laporannya tidak bergantung pada urutan eksekusi.

bash skrip-trap-err.sh
# tidak-dijalankan
# Trap ERR melaporkan baris tempat perintah gagal.

set -euo pipefail

lapor() {
  printf '%s\n' "perintah gagal pada baris $1 dengan kode keluar $2"
}
trap 'lapor "$LINENO" "$?"' ERR

echo "sebelum perintah gagal"
false
echo "tidak pernah tercetak"

Skrip di atas memakai pola itu untuk melaporkan kegagalan pada baris false yang sengaja diletakkan di tengah skrip. Jalankan skripnya sebagai skrip anak seperti sebelumnya.

bash
# jalankan skrip berisi perintah yang gagal
bash skrip-trap-err.sh
echo "kode keluar: $?"
text Keluaran
sebelum perintah gagal
perintah gagal pada baris 12 dengan kode keluar 1
kode keluar: 1

Keluarannya menunjukkan urutan yang dapat diandalkan, yaitu pesan sebelum kegagalan, laporan baris 12 yang merupakan baris false, dan kode keluar 1 yang diteruskan ke pemanggil. Baris tidak pernah tercetak tidak pernah muncul karena set -e menghentikan skrip segera setelah trap ERR selesai dijalankan. Pelaporan seperti ini memangkas waktu pencarian pada skrip panjang, karena lokasi kegagalan diberitahukan tanpa perlu membaca seluruh log.

Pola log yang teratur#

Kedua skrip contoh memakai pola log yang sama sehingga keluarannya mudah dibaca manusia sekaligus mesin. Setiap pesan dilewatkan ke fungsi log yang membungkusnya dengan awalan [INFO], sedangkan pesan kegagalan memakai error_log dengan awalan [ERROR] dan dikirim ke aliran error melalui redirection >&2 (pengalihan keluaran ke tujuan lain). Fungsi kecil seperti ini menggantikan belasan echo yang formatnya berbeda-beda, dan perubahan format kelak cukup dilakukan di satu tempat. Contoh pada topik ini sengaja tidak mencantumkan waktu pencatatan agar keluarannya deterministik dan dapat diverifikasi ulang, sedangkan kebutuhan audit waktu dapat dipenuhi dengan menambahkan pemanggilan date di dalam fungsi log.

Disiplin yang sama berlaku pada isi pesannya, yaitu satu pesan untuk satu kejadian dengan kalimat yang cukup spesifik. Pesan menyalin catatan.txt lebih berguna daripada pesan generik seperti memproses, karena baris log yang gagal dapat menunjuk langkah yang persis bermasalah. Awalan tingkat juga memudahkan penyaringan, misalnya hanya menampilkan baris [ERROR] saat pemecahan masalah. Kedua skrip juga dijaga tetap bebas dari temuan analisis statis.

bash
# pastikan kedua skrip bebas dari temuan analisis statis
shellcheck --shell=bash skrip-arsip.sh && shellcheck --shell=bash skrip-trap-err.sh && echo 'meta: arsip+traperr bersih'
text Keluaran
meta: arsip+traperr bersih

Kesalahan umum#

Kesalahan berikut sering muncul ketika trap baru mulai dipakai, dan ketiganya berpola sama, yaitu asumsi tentang kapan trap mulai bekerja serta apa yang dilakukannya.

  • Memasang trap setelah titik kegagalan. Trap baru mulai berlaku sejak barisnya dieksekusi, sehingga kegagalan yang terjadi di atasnya berlalu tanpa pembersihan. Pasang trap bersih EXIT segera setelah variabel yang dibutuhkan fungsi pembersihan ditetapkan.
  • Membuat fungsi pembersihan yang rumit. Pembersihan yang memuat pekerjaan penting, misalnya mengunggah berkas, dapat gagal dan mengaburkan kode keluar aslinya, karena nilai terakhir yang ditetapkan fungsi itulah yang terbaca pemanggil. Selesaikan pekerjaan penting sebelum trap, dan isi fungsi pembersihan hanya dengan perintah yang aman diulang.
  • Menganggap trap ERR otomatis diwarisi fungsi. Tanpa opsi set -E, kegagalan di dalam fungsi tidak memicu trap ERR yang dipasang di tubuh utama skrip, sehingga kegagalan itu tampak tenang padahal berbahaya. Aktifkan set -E apabila pelaporan kegagalan perlu menjangkau fungsi juga.

Ringkasan#

  • trap bersih EXIT menjamin fungsi pembersihan berjalan pada semua jalur akhir skrip, baik sukses maupun gagal.
  • Trap EXIT tidak mengubah kode keluar skrip, sehingga exit 1 tetap sampai ke pemanggil.
  • Trap ERR dengan $LINENO dan $? melaporkan nomor baris serta kode keluar perintah yang gagal.
  • Fungsi log dan error_log memberi awalan tingkat yang konsisten dan mengirim pesan gagal ke aliran error.

Referensi#