Argumen Tertukar, Budget Terbakar: Validasi Harus di Paling Atas
Set -euo pipefail tidak menyelamatkan skrip dari argumen tertukar. Guard whitelist dan exit code khusus di pintu masuk mencegah budget terbakar.
Ringkasan
Job pertama lancar tapi argumen skrip ternyata tertukar, dana $0.137 kebuang percuma padahal set -euo pipefail udah aktif. Ternyata -u cuma nangkep variabel yang belum didefinisiin, bukan yang isinya salah. Solusinya: guard validasi di paling atas skrip pakai case, lengkap sama exit code khusus biar gagalnya cepat.
Deploy pertama jalan mulus. Tapi pas saya buka log, ternyata job-nya jalan sampai selesai dengan output sampah. Argumen command-line-nya tertukar sejak awal, tapi skrip tetap dieksekusi sampai tuntas karena positional parameters hanya diisi dari argumen skrip saat invocation [2]. Hasilnya? Budget $0.137 terbakar cuma-cuma sebelum ada yang sadar kalau konfigurasi yang dipakai itu salah total.
Tebakan awal saya waktu itu cukup naif. Saya pikir, “Ah, kan udah ada set -euo pipefail di baris kedua, harusnya skrip langsung berhenti kalo ada yang aneh.” Ternyata dugaan itu meleset jauh.
Opsi -u di bash emang berguna. Dia membuat shell langsung exit kalo ada variabel yang unset atau belum didefinisikan sama sekali [1]. Tapi dia sama sekali nggak peduli kalo variabelnya ada, cuma isinya salah tipe, kosong, atau posisinya tertukar dengan argumen lain. Validasi input harus dilakukan di batas kepercayaan, yaitu tepat di awal skrip, sebelum kerja berat apa pun dimulai.
Menutup pintu di awal eksekusi
Solusinya ada di commit terbaru saya: delapan baris guard di bagian paling atas skrip launcher. Community best practice juga menyarankan positional parameters milik skrip dicek di bagian paling atas [4]. Ini bukan sekadar ngecek biasa, tapi penjaga gawang yang memastikan skrip hanya jalan kalo kondisinya benar-benar valid.
Pertama, path recipe dibuat absolute, lalu dicek keberadaannya.
case "$RECIPE" in
/*) ;; # absolute path: use as-is
*) RECIPE="$PWD/$RECIPE" ;; # make absolute
esac
[ -f "$RECIPE" ] || { echo "LAUNCH-REFUSED: recipe not found: $RECIPE" >&2; exit 45; }Kalo file recipe nggak ada, skrip langsung berhenti dengan exit code 45. Angka ini bukan angka acak. Bash udah punya kode yang direservasi: 126 untuk file yang ditemukan tapi nggak bisa dieksekusi, 127 untuk command not found, dan 128+N untuk termination oleh signal [6]. Memilih 45 yang aman di luar rentang reserved itu memungkinkan skrip pemanggil membedakan jenis kegagalan secara spesifik.
Trik whitelist-by-negation
Bagian yang sering bikin bingung adalah validasi angka. Alih-alih mencoba mencocokkan format angka yang benar, saya pakai pendekatan whitelist-by-negation memakai case.
case "$TURNS" in (*[!0-9]*|'') echo "LAUNCH-REFUSED: max-turns must be an integer, got '$TURNS'" >&2; exit 46;; esacPola *[!0-9]* ini kerja dengan cara membalik logika. Kita justru mencari keberadaan karakter yang BUKAN angka di dalam string. Ditambah |'' untuk nangkep kondisi string kosong. Jadi kalo user memasukkan "10a" atau nggak memasukkan apa-apa, pola ini langsung tertangkap dan skrip berhenti dengan exit code 46.
Hal yang sama diterapkan untuk argumen budget yang harus berformat desimal.
case "$BUDGET" in (*[!0-9.]*|'') echo "LAUNCH-REFUSED: budget must be a plain decimal, got '$BUDGET'" >&2; exit 46;; esacSatu catatan penting: case di bash mengeksekusi daftar perintah dari pola yang PERTAMA kali cocok, dipindai dari atas ke bawah [5]. Ini bikin eksekusinya cepat dan deterministik. Nggak perlu regex berat, cukup pola glob standar yang udah dibangun di dalam shell. Dan satu hal lagi: validasi dan mengutip variabel itu saling melengkapi, bukan alternatif. Ekspansi tanpa tanda kutip bisa pecah kena word splitting dan glob expansion [3], jadi guard sekeras apa pun jadi sia-sia kalo variabelnya dikutip ngasal.
Alasan pendekatan ini masuk akal
Mungkin ada yang bertanya, kenapa nggak pakai tools validasi yang lebih canggih? Jawabannya ada di kesederhanaan dan kecepatan. Skrip launcher ini adalah entry point. Dia harus ringan, cepat, dan nggak bergantung pada dependency eksternal kayak Python atau Node.js cuma buat ngecek apakah sebuah string itu angka atau bukan.
Dengan menempatkan validasi di paling atas, kita memotong eksekusi yang nggak perlu. Mesin nggak perlu mengalokasikan memori atau memulai proses child kalo sejak awal datanya udah salah. Caller script juga bisa menangkap exit code 45 atau 46 lalu memberikan pesan error yang jauh lebih ramah ke user. Misalnya, orchestration script bisa langsung mengirim notifikasi ke channel Slack bahwa file konfigurasi hilang, alih-alih membiarkan sistem gagal di tengah jalan dengan trace error yang membingungkan.
Saya awalnya condong membiarkan bash menangani error secara natural lewat set -e. Tapi setelah melihat sendiri gimana mudahnya argumen tertukar lolos dari radar itu, saya putuskan ngubah pendekatan. Validasi fail fast di pintu masuk memastikan skrip hanya berjalan ketika kondisinya benar-benar aman, dan itu satu-satunya cara yang masuk akal buat melindungi sistem dari input yang nggak terduga.