Exit Code dan set -e

Disiplin penanganan error: kode kembali, $?, dan set -e.

Daftar isi

Setiap perintah yang dijalankan bash berakhir dengan sebuah angka yang menyatakan hasilnya, dan angka itu disebut kode kembali (exit code), sedangkan dokumentasi lain menyebutnya status keluar. Angka ini menjadi satu-satunya saluran standar bagi program untuk melaporkan keberhasilan tanpa mencetak apa pun, sehingga seluruh otomatisasi bertumpu padanya. Topik ini membahas pembacaan kode kembali dengan $?, penetapannya dengan exit, serta disiplin set -e yang menghentikan skrip pada kegagalan pertama.

Kode kembali dan $?#

Status perintah terakhir dapat dibaca melalui parameter khusus $?. Perintah true selalu berhasil sehingga statusnya nol, sedangkan false selalu gagal dengan status satu. Keduanya menjadi alat uji yang ringkas untuk melihat mekanismenya:

bash
# kode kembali perintah yang berhasil
true
echo $?
# kode kembali perintah yang gagal
false
echo $?
text Keluaran
0
1

Konvensinya, nol berarti berhasil dan angka satu sampai 255 berarti gagal, dengan rincian angkanya ditetapkan masing-masing program. Rentang terbatas itu karena saluran statusnya hanya satu byte, sehingga angka di luar 255 tidak mungkin dilaporkan apa adanya. Perintah ls pada berkas yang tidak ada, misalnya, berakhir dengan status 2:

bash
# kegagalan ls pada berkas yang tidak ada
ls /tidak-ada-2607
echo $?
text Keluaran
ls: cannot access '/tidak-ada-2607': No such file or directory
2

bash sendiri menetapkan status 127 untuk perintah yang tidak ditemukannya dan 126 untuk berkas yang tidak dapat dieksekusi. Kedua keadaan itu terdeteksi sebelum program mana pun berjalan, sehingga pesannya berasal dari bash dan bukan dari perintah yang dipanggil. Nilai-nilai inilah yang dibaca alat di sekitar skrip, seperti penjadwal tugas atau alat integrasi berkelanjutan, untuk memutuskan apakah satu langkah dianggap berhasil.

Skrip yang melaporkan keberhasilan#

Skrip juga berakhir dengan sebuah kode kembali, dan nilainya adalah status perintah terakhir yang dijalankannya. Sifat ini membuat skrip dapat digunakan sebagai syarat oleh pemanggilnya tanpa perlu mengurai keluarannya. Skrip berikut hanya memeriksa keberadaan satu berkas arsip:

bash cek-status.sh
#!/bin/bash
# memeriksa keberadaan berkas arsip cadangan

echo "Memeriksa berkas arsip..."
test -f "$1"
text Keluaran
Memeriksa berkas arsip...

Dipanggil tanpa argumen, skrip mencetak satu baris lalu berakhir dengan status satu karena test -f pada argumen kosong gagal, meskipun kegagalannya tidak tampak di keluaran. Status itulah isi sebenarnya dari skrip ini. Uji berikut menyiapkan folder kerja lalu menjalankan skrip pada berkas yang ada dan yang tidak ada:

bash
# folder kerja latihan
mkdir -p /tmp/kerja-exit-code-dan-set-e
# menyalin tiga skrip topik ini ke folder kerja
cp cek-status.sh tanpa-set-e.sh dengan-set-e.sh /tmp/kerja-exit-code-dan-set-e/
cd /tmp/kerja-exit-code-dan-set-e
bash
# menjalankan skrip pada berkas yang ada dan yang tidak ada
touch cadangan.tar.gz
bash cek-status.sh cadangan.tar.gz
echo "kode kembali: $?"
bash cek-status.sh hilang.tar.gz
echo "kode kembali: $?"
text Keluaran
Memeriksa berkas arsip...
kode kembali: 0
Memeriksa berkas arsip...
kode kembali: 1

Status yang dibaca $? setelah bash cek-status.sh adalah status skripnya sendiri, bukan status echo di dalamnya. Banyak skrip memang mencetak pesan untuk manusia, tetapi penilaiannya tetap diserahkan kepada statusnya. Pola ini menjadi dasar perangkaian skrip, yaitu satu skrip dapat memeriksa hasil skrip lain lewat if tanpa membaca satu baris keluarannya pun.

exit dan penugasan kode kembali#

Pernyataan exit menghentikan skrip seketika dan menetapkan kode kembalinya. Bentuk exit 0 menyatakan keberhasilan secara eksplisit, sedangkan exit 1 sampai exit 255 menyatakan kegagalan dengan nomor yang ditetapkan penulis skrip. Perintah apa pun yang tertulis setelah exit tidak pernah dijalankan.

Dua skrip kecil berikut berbeda hanya pada baris exit-nya, dan perbedaan itu terbaca dari luar:

bash
# skrip kecil yang berakhir dengan exit 0
printf '#!/bin/bash\necho "Tugas selesai"\nexit 0\n' > tugas-sukses.sh
bash tugas-sukses.sh
echo "kode kembali: $?"
# skrip kecil yang berakhir dengan exit 1
printf '#!/bin/bash\necho "Tugas dimulai"\nexit 1\n' > tugas-gagal.sh
bash tugas-gagal.sh
echo "kode kembali: $?"
text Keluaran
Tugas selesai
kode kembali: 0
Tugas dimulai
kode kembali: 1

Skrip kedua sempat mencetak Tugas dimulai, tetapi exit 1 di baris berikutnya menghentikannya sebelum baris lain sempat dijalankan. Perhatikan pula bahwa status yang terbaca adalah 1, persis angka yang diberikan ke exit. Pada skrip sungguhan, exit digunakan di dalam percabangan, misalnya exit 1 ketika argumen wajib tidak disediakan.

Disiplin set -e#

Secara bawaan, bash melanjutkan eksekusi ke baris berikutnya meskipun perintah sebelumnya gagal, dan skrip panjang paling sering rusak karena perilaku ini. Opsi set -e, yang disebut juga errexit, mengubah perilaku tersebut: skrip berhenti pada perintah sederhana pertama yang gagal. Dua skrip berikut melakukan pekerjaan yang sama pada berkas yang memang tidak ada, satu tanpa disiplin dan satu dengan disiplin.

bash tanpa-set-e.sh
#!/bin/bash
# menyalin arsip tanpa disiplin set -e

echo "Langkah 1: memeriksa berkas sumber"
ls arsip-hilang.tar.gz
echo "Langkah 2: menyalin arsip"
cp arsip-hilang.tar.gz /tmp/arsip.tar.gz
echo "Langkah 3: arsip tersalin"
text Keluaran
Langkah 1: memeriksa berkas sumber
ls: cannot access 'arsip-hilang.tar.gz': No such file or directory
Langkah 2: menyalin arsip
cp: cannot stat 'arsip-hilang.tar.gz': No such file or directory
Langkah 3: arsip tersalin

Dua perintah gagal dengan pesan error, tetapi skrip tetap sampai ke baris terakhir dan mengklaim arsip tersalin. Bagi manusia yang membaca terminal, errornya terlihat, sedangkan bagi penjadwal tugas yang hanya membaca status, skrip ini berakhir sukses. Sekarang bandingkan dengan versi yang diberi satu baris set -e:

text dengan-set-e.sh
#!/bin/bash
# menyalin arsip dengan disiplin set -e

set -e

echo "Langkah 1: memeriksa berkas sumber"
ls arsip-hilang.tar.gz
echo "Langkah 2: menyalin arsip"
cp arsip-hilang.tar.gz /tmp/arsip.tar.gz
echo "Langkah 3: arsip tersalin"
bash
# skrip dengan set -e berhenti pada kegagalan pertama
bash dengan-set-e.sh
text Keluaran
Langkah 1: memeriksa berkas sumber
ls: cannot access 'arsip-hilang.tar.gz': No such file or directory

Eksekusi berhenti tepat setelah ls gagal, sehingga Langkah 2 dan Langkah 3 tidak pernah berjalan dan salinan palsu tidak pernah diklaim. Status skripnya satu, sehingga pemanggilnya tahu pekerjaan ini gagal. Satu baris set -e pada bagian atas skrip adalah disiplin murah yang mencegah kegagalan disusul klaim sukses.

Pengecualian set -e#

set -e tidak memicu penghentian pada setiap kegagalan. Perintah yang digunakan sebagai syarat if atau while dikecualikan karena kegagalannya memang sedang diperiksa, demikian pula perintah di dalam rantai && atau ||, kecuali perintah terakhir dalam rantai itu. Aturan ini membuat percabangan tetap bekerja normal di bawah disiplin errexit:

bash
# kegagalan di dalam syarat if tidak menghentikan skrip
bash -c 'set -e; if ls /tidak-ada; then echo "berkas ada"; else echo "berkas tidak ada"; fi; echo "skrip tuntas"'
text Keluaran
ls: cannot access '/tidak-ada': No such file or directory
berkas tidak ada
skrip tuntas

ls gagal dan mencetak errornya, tetapi kegagalan itu justru mengalirkan eksekusi ke cabang else, dan skrip tuntas sampai baris terakhirnya. Pesan errornya tetap tampil karena set -e hanya mengubah keputusan berhenti, bukan meredam keluaran error. Perilaku yang sama berlaku pada rantai &&. Kegagalan pada cabang awalnya tidak memicu penghentian:

bash
# kegagalan cabang awal rantai && tidak memicu penghentian
bash -c 'set -e; false && echo "tidak tercetak"; echo "skrip tuntas"'
text Keluaran
skrip tuntas

Dua pengecualian inilah yang membuat set -e aman digunakan bersama percabangan. Namun, pengecualian yang sama juga menjelaskan mengapa sebagian kegagalan luput, karena perintah yang dijepit && tidak pernah menghentikan skrip meskipun gagal. Pemeriksaan yang penting sebaiknya ditulis sebagai pernyataan if yang eksplisit, bukan diselipkan di dalam rantai.

Kesalahan umum#

  • Membaca $? setelah menjalankan perintah lain. Statusnya sudah tertimpa, sehingga simpan ke variabel segera setelah perintah yang diperiksa.
  • Menjalankan exit tanpa angka lalu mengira skripnya berakhir sukses. exit tanpa angka mewarisi status perintah terakhir, yang bisa saja bukan nol.
  • Mengira set -e menghentikan segala kegagalan. Perintah di dalam syarat if, while, serta rantai && atau || dikecualikan, sehingga kegagalan di sana harus diperiksa secara sadar.
  • Membuang pesan error dengan 2>/dev/null tanpa memeriksa statusnya. Keluaran errornya hilang dan statusnya diabaikan, sehingga kegagalan tidak menyisakan jejak apa pun.
  • Menutup skrip tanpa memastikan status akhirnya. Perintah terakhir yang gagal, misalnya grep yang tidak menemukan apa pun, membuat skrip berakhir gagal meskipun pekerjaan intinya selesai, sehingga exit 0 perlu ditetapkan secara eksplisit bila skripnya memang selesai dengan sukses.

Ringkasan#

  • Status perintah terakhir terbaca lewat $?, dengan konvensi nol untuk berhasil dan angka satu sampai 255 untuk gagal.
  • Skrip mewarisi status perintah terakhirnya, dan exit menetapkannya sekaligus menghentikan eksekusi seketika.
  • set -e menghentikan skrip pada perintah sederhana pertama yang gagal sehingga kegagalan tidak disusul klaim sukses.
  • Perintah di dalam syarat if, while, serta rantai && atau || dikecualikan dari disiplin set -e.

Referensi#