Home / Programming Languages, Databases, and Software Development / Bash yang Aman: …

Bash yang Aman: Quoting, set -euo pipefail, dan Jebakan Ekspansi

Menulis skrip Bash sering kali terasa mudah sampai skrip tersebut dijalankan di server produksi dengan path yang mengandung spasi, dan tiba-tiba sistem menghapus folder yang salah atau gagal menemukan berkas. Sebagian besar bug “invisible” di Bash bukan berasal dari logika yang salah, melainkan dari perilaku default shell yang terlalu permisif terhadap ekspansi variabel.

Artikel ini membahas cara mengubah Bash dari “alat yang mencoba menebak maksud Anda” menjadi alat yang ketat dan prediktif, menggunakan praktik quoting yang benar dan flag keselamatan set -euo pipefail.

Ringkasan

  • Quoting variabel dengan "$VAR" menghentikan word splitting dan globbing.
  • set -e menghentikan eksekusi segera saat perintah gagal (non-zero exit).
  • set -u mencegah penggunaan variabel yang tidak terdefinisi (unbound).
  • set -o pipefail memastikan kegagalan di tengah pipeline tidak tersembunyi.
  • Array "${list[@]}" adalah satu-satunya cara aman mengelola daftar argumen.
  • bash -n digunakan untuk verifikasi sintaks tanpa menjalankan skrip.

Lingkungan Pengujian

text
$ bash --version
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
$ id -u
1000
$ uname -srmo
Linux 7.0.0-31-generic x86_64 GNU/Linux

Semua contoh diuji pada Ubuntu 26.04.1 LTS menggunakan bash 5.3.9.

Jebakan Terbesar: Word Splitting dan Globbing

Masalah paling umum di Bash adalah ketika developer menganggap variabel berperilaku seperti string di bahasa pemrograman modern. Di Bash, ekspansi variabel tanpa kutip dilakukan melalui dua tahap: Word Splitting dan Globbing.

Bagaimana Word Splitting Bekerja

Ketika Bash melihat $VAR (tanpa kutip), ia mengambil nilai variabel tersebut dan membaginya menjadi beberapa kata berdasarkan karakter di IFS (Internal Field Separator). Secara default, IFS berisi spasi, tab, dan newline. Proses ini terjadi sebelum argumen dikirim ke perintah.

bash
# Skenario: Path mengandung spasi
FILE="my file.txt"
ls $FILE

Output:

text
ls: cannot access 'my': No such file or directory
ls: cannot access 'file.txt': No such file or directory

Bash tidak melihat satu argumen "my file.txt", melainkan dua argumen terpisah: my dan file.txt. Ini terjadi karena Bash melakukan splitting pada spasi di dalam nilai $FILE.

Bahaya Globbing (Pathname Expansion)

Setelah pembagian kata, Bash memeriksa apakah ada karakter wildcard (*, ?, [) di setiap kata. Jika ada dan cocok dengan berkas di disk, kata tersebut diganti dengan daftar berkas yang cocok.

bash
# Skenario: Variabel berisi karakter wildcard
VAR="*"
ls $VAR

Alih-alih mencari berkas bernama literal *, Bash akan menampilkan seluruh isi direktori saat ini. Jika variabel ini digunakan dalam perintah destruktif seperti rm -rf $VAR, hasilnya bisa berupa penghapusan seluruh isi folder secara tidak sengaja.

Solusi: Quoting yang Benar

Kutip ganda ("...") memberitahu Bash untuk memperlakukan hasil ekspansi sebagai satu kata tunggal dan mengabaikan globbing, tetapi tetap mengizinkan ekspansi variabel dan command substitution.

TeknikHasilEfek
$VARUnquotedWord splitting + Globbing aktif
"$VAR"Double quotedSatu kata, no splitting, no globbing
'$VAR'Single quotedLiteral, tidak ada ekspansi sama sekali

Contoh perbaikan:

bash
FILE="my file.txt"
ls "$FILE" # Sukses: mencari satu berkas "my file.txt"

Sesuai dengan standar bash(1), quoting adalah cara utama untuk menjamin integritas data saat dilewatkan sebagai argumen.

Menghidupkan “Safety Net” dengan set -euo pipefail

Secara default, Bash akan terus berjalan meskipun perintah sebelumnya gagal. Ini sangat berbahaya dalam otomasi karena kegagalan di awal langkah dapat menyebabkan perintah berikutnya berjalan dengan data yang salah. Flag set -euo pipefail mengubah perilaku ini menjadi “gagal dengan cepat” (fail-fast).

set -e (errexit)

set -e memaksa shell keluar segera jika ada perintah yang mengembalikan status non-zero.

bash
#!/usr/bin/env bash
set -e
ls /folder/yang/tidak/ada
echo "Pesan ini tidak akan pernah tercetak"

Catatan Penting: set -e tidak memicu keluar jika perintah gagal di dalam kondisi if, elif, while, until, atau perintah yang dihubungkan dengan && atau ||. Hal ini memungkinkan Anda untuk menangani error secara eksplisit:

bash
# Tidak akan keluar karena kegagalan ditangani oleh ||
ls /nonexistent || echo "Folder tidak ditemukan, tapi saya tetap jalan"

set -u (nounset)

set -u membuat skrip keluar segera jika mencoba menggunakan variabel yang belum diberi nilai (unbound).

bash
#!/usr/bin/env bash
set -u
echo "Halo $NAMA_SAYA"

Output: bash: NAMA_SAYA: unbound variable. Tanpa set -u, Bash menganggap variabel tidak terdefinisi sebagai string kosong. Bayangkan skrip pembersihan menggunakan rm -rf $TARGET_DIR/. Jika $TARGET_DIR kosong, perintah menjadi rm -rf /, yang akan menghapus seluruh sistem root jika dijalankan sebagai root.

Untuk kasus di mana variabel memang boleh kosong, gunakan sintaks default value: ${NAMA_SAYA:-Tamu}.

set -o pipefail

Secara default, return status sebuah pipeline adalah status perintah terakhir. Jika perintah pertama gagal tetapi perintah terakhir sukses, seluruh pipeline dianggap sukses.

bash
# Tanpa pipefail: return status adalah 0 (sukses)
ls /nonexistent | cat
echo "Selesai tanpa error"

Dengan set -o pipefail, return status pipeline adalah nilai dari perintah terakhir yang gagal. Jika dikombinasikan dengan set -e, skrip akan berhenti seketika saat bagian mana pun dari pipeline gagal.

bash
#!/usr/bin/env bash
set -e
set -o pipefail
ls /nonexistent | cat
echo "Pesan ini tidak akan terlihat"

Sesuai dengan spesifikasi POSIX Shell Command Language, perilaku default pipeline sering kali menjadi penyebab utama bug yang sulit dideteksi di lingkungan produksi.

Mengelola Daftar Argumen dengan Array

Salah satu kesalahan umum adalah menyimpan daftar berkas dalam string yang dipisahkan spasi. Ini rentan terhadap word splitting jika ada berkas dengan nama mengandung spasi.

Solusi: Bash Arrays

Gunakan array untuk menyimpan elemen, lalu ekspansi menggunakan "${list[@]}". Ekspansi ini menjamin bahwa setiap elemen array tetap menjadi satu argumen terpisah, terlepas dari jumlah spasi di dalamnya.

bash
#!/usr/bin/env bash
# SALAH: Menggunakan string (rentan word splitting)
files="file 1.txt file 2.txt"
for f in $files; do
    echo "Prosesing $f" # Akan memproses 'file', '1.txt', dll.
done

# BENAR: Menggunakan array
files=("file 1.txt" "file 2.txt")
for f in "${files[@]}"; do
    echo "Prosesing $f" # Berhasil memproses "file 1.txt" utuh
done

Perbedaan antara "${list[@]}", "${list[*]}", dan $@ adalah krusial:

  • "${list[@]}": Ekspansi menjadi beberapa kata, masing-masing dikutip. (Sangat Direkomendasikan).
  • "${list[*]}": Ekspansi menjadi satu kata tunggal, semua elemen digabung dengan spasi.
  • $@: Ekspansi menjadi beberapa kata, tapi rentan word splitting jika elemen mengandung spasi.

Verifikasi Sintaks dan Analisis Statis

Sebelum menjalankan skrip di server produksi, gunakan flag -n untuk memeriksa sintaks tanpa benar-benar mengeksekusi perintah.

bash
bash -n my_script.sh

Jika tidak ada output, sintaks skrip valid. Namun, bash -n tidak bisa mendeteksi kesalahan logika atau kegagalan runtime. Untuk analisis yang lebih mendalam, gunakan alat statis seperti ShellCheck. ShellCheck dapat mendeteksi pola berbahaya seperti penggunaan $VAR tanpa kutip (SC2086) atau penggunaan set -e yang tidak konsisten.

Rekomendasi Praktis

  • Selalu mulai skrip dengan set -euo pipefail untuk memastikan kesalahan tidak terabaikan.
  • Kutip semua ekspansi variabel: gunakan "$VAR", bukan $VAR.
  • Gunakan array () dan ekspansi "${list[@]}" untuk mengelola daftar argumen secara aman.
  • Hindari echo untuk mencetak variabel yang mungkin mengandung opsi; gunakan printf.
  • Gunakan [[ ... ]] untuk pengujian kondisi daripada [ ... ] karena lebih kuat, mendukung regex, dan tidak rentan word splitting.
  • Jalankan bash -n sebelum commit untuk memastikan tidak ada typo pada struktur kontrol.