set -euo pipefail adalah tiga pengaman sekaligus: -e menghentikan script saat perintah mengembalikan status bukan nol, -u menganggap variabel yang belum didefinisikan sebagai kesalahan, dan pipefail membuat status akhir pipeline ditentukan perintah yang gagal, bukan hanya perintah terakhir. Pada pengujian langsung di host ini, script dengan set -e berhenti pada baris ((i++)) ketika nilai awalnya nol dan keluar dengan status 1, sedangkan pipeline false | true tetap keluar dengan status 0 sebelum pipefail diaktifkan. Kedua hasil itu menjelaskan mengapa trio ini berguna sekaligus mudah disalahpahami.
Artikel ini membedah jebakan yang paling sering ditemui, menunjukkan status keluar yang terukur, dan menutup dengan pola penulisan script yang gagal secara jelas alih-alih gagal secara diam-diam.
TL;DR
- set -e berhenti pada ekspresi aritmetika yang bernilai nol, karena nilainya dipakai sebagai status keluar.
- pipefail mengubah status pipeline; tanpa itu kegagalan di tengah pipeline tidak terlihat.
- Status 127 berarti perintah tidak ditemukan, dan 141 berasal dari sinyal pipe (128 ditambah 13).
- PIPESTATUS menyimpan status tiap perintah dalam pipeline, terpisah dari status pipeline.
- Perintah yang sengaja boleh gagal harus ditulis eksplisit, bukan dibiarkan mengandalkan perilaku set -e.
Mengapa script berhenti pada ((i++))?
Karena ekspresi aritmetika mengembalikan status keluar yang sama dengan nilainya, dan nilai nol dianggap gagal oleh shell. Pada pengujian di host ini, script berisi set -e lalu i=0 lalu ((i++)) berhenti sebelum baris berikutnya tercetak, dan status keluarnya 1. Setelah variabel naik menjadi satu, ekspresi yang sama mengembalikan status 0 sehingga script berlanjut.
Perilaku ini dijelaskan di manual bash pada bagian tentang perintah aritmetika dan status keluar. Rujukan lengkapnya ada di manual bash di gnu.org. Bagi penulis script, konsekuensinya praktis: hindari bentuk ((i++)) sebagai pernyataan berdiri sendiri di script yang memakai set -e, dan pilih bentuk penugasan yang selalu mengembalikan status 0.
set -e
i=0
i=$((i + 1))
: "$i"
Bentuk penugasan di atas menyimpan hasil tanpa memakai nilai ekspresi sebagai status keluar. Perbedaan satu baris ini menjelaskan sebagian besar laporan "script berhenti tanpa alasan" pada script yang sebenarnya benar.
Kapan set -e tidak menyelamatkan script?
set -e tidak berlaku pada semua konteks. Perintah yang menjadi bagian dari kondisi if, while, atau until tidak menghentikan script ketika gagal, karena kegagalan itu memang sedang diperiksa. Perintah yang diikuti operator || juga tidak menghentikan script, dan perintah di dalam pipeline hanya berpengaruh pada status akhir pipeline kecuali pipefail diaktifkan.
Pada pengujian di host ini, fungsi yang isinya false lalu echo langsung menghentikan script saat dipanggil dengan set -e aktif, dan status keluarnya 1. Artinya penghentian memang merambat keluar dari fungsi, tetapi bukan dari setiap perintah di dalamnya: perintah yang berada di posisi kondisi tetap tidak menghentikan apa pun.
Karena itu, script yang mengandalkan set -e tanpa memahami konteks sering tampak aman tetapi menyimpan kegagalan di dalam kondisi. Untuk pekerjaan yang harus berhenti pada kegagalan apa pun, tulis pemeriksaan eksplisit setelah perintah yang penting, lalu tangani sendiri kegagalannya.
Bagaimana pipefail dan PIPESTATUS berbeda?
pipefail mengubah status akhir pipeline menjadi status perintah pertama yang gagal, sedangkan PIPESTATUS menyimpan status setiap perintah dalam pipeline sebagai larik. Pada pengujian di host ini, false | true keluar dengan status 0, tetapi PIPESTATUS untuk perintah pertama bernilai 1; setelah pipefail diaktifkan, pipeline yang sama keluar dengan status 1.
| Konstruksi | Status keluar terukur | Arti praktis |
|---|---|---|
| false | true tanpa pipefail | 0 | Kegagalan di tengah pipeline tidak terlihat |
| false | true dengan pipefail | 1 | Pipeline dianggap gagal bila ada anggotanya gagal |
| PIPESTATUS[0] pada contoh di atas | 1 | Status perintah pertama tetap bisa dibaca |
| Perintah yang tidak ada | 127 | Biner tidak ditemukan di PATH |
| Proses dihentikan sinyal pipe | 141 | 128 ditambah nomor sinyal 13 |
Perilaku PIPESTATUS dan pipefail diatur di manual bash dan di spesifikasi POSIX untuk shell. Spesifikasi POSIX menyediakan rujukan normatif yang lebih ketat, dan tautannya ada di spesifikasi shell POSIX di pubs.opengroup.org.
Apakah status 127 dan 141 menandakan bug?
Tidak selalu. Status 127 berarti perintah tidak ditemukan, yang biasanya menandakan salah ketik nama perintah atau PATH yang berbeda antara lingkungan pengembangan dan lingkungan eksekusi. Status 141 berasal dari sinyal pipe, dan sering muncul pada pipeline yang perintah pertamanya masih menulis ketika perintah berikutnya sudah selesai dan menutup saluran.
Status 141 kerap dilaporkan sebagai kegagalan yang membingungkan karena perintahnya sendiri tidak salah. Contoh klasiknya adalah perintah yang menghasilkan keluaran panjang lalu dipotong oleh head: yang berhenti karena sinyal adalah produsen, bukan konsumen, sehingga pipefail akan melaporkan pipeline itu sebagai gagal meski keluaran yang diminta sudah didapat.
Untuk kasus semacam itu, keputusan yang jujur adalah menyatakan pengecualian secara eksplisit. Menyembunyikan status dengan menambahkan operator || true di ujung pipeline memang membuat script berhenti mengeluh, tetapi juga menghapus satu-satunya sinyal yang tersedia ketika kegagalan sungguhan terjadi.
Bagaimana menulis script yang gagal dengan jelas?
Mulailah dengan empat baris pembuka yang eksplisit: shebang, set -euo pipefail, dan dua baris yang menangani status keluar serta baris tempat kegagalan terjadi. Tanpa penangan status, script yang berhenti hanya memberi status 1 tanpa petunjuk baris mana yang gagal, dan waktu diagnosis menjadi jauh lebih panjang daripada waktu menulis penanganannya.
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "gagal di baris $LINENO" >&2' ERR
Setelah itu, tulis perintah yang sengaja boleh gagal secara eksplisit. Perintah semacam itu sebaiknya dibungkus kondisi if atau diberi operator || dengan penanganan yang jelas, sehingga tidak ada perintah yang statusnya diabaikan tanpa niat. Kebiasaan ini juga membuat pembaca script bisa membedakan perintah wajib dari perintah opsional tanpa harus menelusuri seluruh alur.
Langkah berikutnya adalah memeriksa variabel sebelum dipakai. Dengan -u aktif, variabel kosong yang tidak didefinisikan langsung menghentikan script, dan itu lebih baik daripada melanjutkan dengan nilai kosong yang menyebar ke perintah berikutnya. Untuk variabel yang memang boleh kosong, tulis nilai bawaannya secara eksplisit agar niatnya terbaca.
Di mana membaca lebih lanjut soal jebakan set -e?
Rujukan praktis yang banyak dipakai adalah kumpulan tanya jawab komunitas tentang perilaku set -e pada berbagai konteks, termasuk daftar kasus yang tidak menghentikan script. Tautannya ada di BashFAQ 105 di wiki wooledge. Halaman itu bukan spesifikasi, tetapi berguna sebagai daftar periksa karena mengumpulkan kasus yang jarang muncul di dokumentasi resmi.
Untuk keputusan yang menyangkut perilaku antar shell, POSIX tetap rujukan utama, sementara manual bash menjelaskan tambahan yang tidak diatur POSIX, termasuk pipefail dan PIPESTATUS. Kombinasi keduanya biasanya cukup untuk menjawab pertanyaan apakah script akan berhenti pada suatu baris.
Yang tidak bisa dijawab dokumentasi adalah perilaku script pada host tertentu. Karena itu, uji perilaku yang diandalkan dengan script kecil sebelum memasukkannya ke pipeline produksi, dan catat status keluar yang terukur. Status keluar yang dicatat jauh lebih berguna daripada asumsi tentang status keluar yang seharusnya.
Bagaimana set -u mengubah perilaku script lama?
set -u membuat variabel yang belum didefinisikan menjadi kesalahan, bukan nilai kosong. Pada script lama yang mengandalkan variabel kosong sebagai nilai bawaan, perubahan ini terasa sebagai kegagalan mendadak yang menunjuk baris pemakaian, bukan baris pendefinisian. Gejalanya mudah dikenali: pesan menyebut nama variabel yang tidak terikat.
Jalur perbaikannya bukan mencabut set -u, melainkan menyatakan nilai bawaan secara eksplisit untuk setiap variabel yang memang boleh kosong. Pola yang paling mudah dibaca adalah menuliskan nilai bawaan di tempat variabel dipakai, sehingga pembaca script tahu apa yang terjadi ketika variabel itu tidak disetel.
Untuk variabel yang wajib ada, pemeriksaan di awal script jauh lebih baik daripada menunggu kegagalan di tengah jalan. Pemeriksaan itu memberi satu pesan yang jelas saat start, alih-alih pesan menyesatkan yang muncul setelah sebagian pekerjaan sempat berjalan setengah jadi.
Kapan pengaman ini justru menambah masalah?
Pada script yang menangani kegagalan sebagian dengan sengaja, set -e bisa menghentikan alur lebih cepat daripada yang dirancang. Contohnya skrip pembersihan yang mencoba menghapus beberapa berkas dan menganggap berkas yang tidak ada sebagai kondisi normal: dengan set -e, percobaan pertama yang gagal menghentikan seluruh pembersihan.
Jalur keluarnya adalah membuat niat itu terbaca. Bila kegagalan sebuah perintah dapat diterima, tuliskan penanganannya di tempat yang sama, lengkap dengan alasan singkat. Script yang menyembunyikan kegagalan secara global terlihat lebih rapi, tetapi kehilangan satu-satunya petunjuk yang berguna saat masalah nyata muncul.
Satu kebiasaan terakhir yang layak dipertahankan: catat status keluar yang terukur ketika script diuji, bukan hanya tulisannya. Catatan itu menjadi rujukan saat perilaku script berubah setelah pembaruan shell atau perubahan lingkungan eksekusi.
FAQ
Apakah set -e selalu menghentikan script pada perintah yang gagal?
Tidak. Perintah di dalam kondisi if atau while, perintah yang diikuti operator ||, dan perintah di dalam pipeline tidak selalu menghentikan script. Penghentian bergantung pada konteks dan pada apakah pipefail diaktifkan.
Mengapa ((i++)) membuat script berhenti padahal terlihat benar?
Karena nilai ekspresi aritmetika dipakai sebagai status keluar, dan nilai nol dianggap gagal. Ketika variabel awal bernilai nol, ekspresi itu mengembalikan 1 sehingga set -e menghentikan script; gunakan bentuk penugasan agar tidak terpengaruh.
Apa arti status keluar 127?
Perintah tidak ditemukan. Penyebab paling umum adalah salah ketik nama perintah atau PATH yang berbeda antara lingkungan pengembangan dan lingkungan eksekusi, termasuk saat script dijalankan dari cron dengan PATH minimal.
Apakah pipefail membuat semua pipeline gagal lebih sering?
Ya, dan itu memang tujuannya. Dengan pipefail, pipeline dianggap gagal bila ada anggotanya gagal, termasuk produsen yang berhenti karena sinyal pipe. Perilaku itu menyingkap kegagalan yang sebelumnya tersembunyi di tengah pipeline.
Apakah aman menambahkan || true pada setiap perintah?
Tidak. Operator itu menghapus sinyal kegagalan dan membuat script melanjutkan tanpa tahu ada masalah. Gunakan hanya pada perintah yang kegagalannya memang dapat diterima, dan sertakan alasan singkat di dekat baris tersebut.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬