Membedah mekanisme quoting di BASH: kapan memakai kutip tunggal, kutip ganda, atau backslash, bagaimana word splitting bekerja dengan IFS, dan mengapa "$@" selalu lebih aman daripada $@ — dengan contoh nyata file bernama ber-spasi yang sering merusak skrip.

Setelah di episode 3 sebelumnya kita membedah variabel — deklarasi tanpa spasi, akses $VAR vs ${VAR}, export untuk mewariskan nilai, dan variabel bawaan seperti $PATH dan $HOME — pada episode kali ini kita membahas topik yang paling sering menjadi sumber bug halus di BASH: quoting, escaping & word splitting.
Inilah perbedaan antara pemula yang skripnya "kadang jalan, kadang tidak" dan praktisi yang skripnya selalu benar. Mengapa echo $nama bisa memecah nama menjadi dua kata? Mengapa rm $FILE bisa menghapus file yang salah ketika nama file mengandung spasi? Mengapa "$@" lebih aman daripada $@? Semua pertanyaan ini berakar pada satu mekanisme: cara BASH memecah string menjadi kata-kata — dan bagaimana kutip mengendalikannya.
Bayangkan BASH sebagai resepsionis yang sangat patuh. Setiap kali kalian memberikan sebuah string, dia memecahnya berdasarkan spasi menjadi "kata-kata" lalu meneruskan setiap kata sebagai argumen terpisah. Kadang itu yang kalian mau — kadang itu bencana. Kutip adalah cara kalian memberi instruksi tegas kepada resepsionis itu: "ini satu entitas utuh, jangan dipecah."
BASH memberi tiga alat untuk mengendalikan interpretasi string:
| Alat | Arti | Contoh |
|---|---|---|
Kutip tunggal '...' | Literal penuh — tidak ada expansion sama sekali | '$HOME tidak diekspansi' |
Kutip ganda "..." | Expansion variabel & command, tapi tidak word splitting & globbing | "$HOME tidak dipecah" |
Backslash \ | Meloloskan satu karakter berikutnya | \$HOME = literal $HOME |
Mari lihat perbedaannya secara nyata:
echo '$HOME adalah literal'
echo "$HOME adalah expansion"
echo \$HOME hanya dolar-nya diloloskan$HOME adalah literal
/home/arman adalah expansion
$HOME hanya dolar-nya diloloskanAturan praktisnya:
$, tidak ada backtick, tidak ada apa pun yang diinterpretasi. Cocok untuk regex, string SQL, atau teks dengan banyak simbol.$VAR) dan command substitution ($(cmd)) terjadi, tetapi ingin hasilnya tetap satu kata — tidak dipecah oleh spasi.\*, \$, \", atau spasi dalam nama file (My\ File).Di dalam kutip ganda, tiga hal tetap terjadi — dan inilah yang membuatnya berbeda dari kutip tunggal:
file="laporan $(date +%Y).txt" # command substitution jalan
dir="~/$USER/docs" # variabel jalan, tapi "~" TIDAK diekspansi
star="a*b*" # glob TIDAK diperluasPerhatikan dua kejutan kecil:
~ tidak diekspansi di dalam kutip. "~/$USER/docs" menghasilkan string literal ~/arman/docs — bukan /home/arman/arman/docs. Tilde hanya bekerja di posisi khusus (awal kata, tanpa kutip).*, ?) tidak diperluas di dalam kutip ganda. "*.log" tetap string *.log — tidak menjadi daftar file. Inilah mengapa globbing butuh "tanpa kutip".| Ekspansi | Dalam '...' | Dalam "..." | Tanpa kutip |
|---|---|---|---|
$VAR variabel | Tidak | Ya | Ya |
$(cmd) substitution | Tidak | Ya | Ya |
Glob * ? | Tidak | Tidak | Ya |
| Word splitting | Tidak | Tidak | Ya |
Tilde ~ | Tidak | Tidak | Ya |
Kutip ganda adalah "zona aman": variabel diekspansi, tapi hasilnya dilindungi dari pemecahan dan globbing. Inilah mengapa para praktisi hampir selalu menulis "$VAR" — bukan $VAR.
Note
Salah satu jebakan yang paling sering membuat pemula bingung: globbing. ls *.log di terminal menampilkan file log karena shell yang memperluas pola sebelum ls dijalankan. Tapi di dalam "*.log", pola tidak diperluas. Jika kalian butuh hasil glob disimpan sebagai daftar, gunakan array (dibahas di episode array) — bukan string yang di-quote.
Sekarang kita sampai pada mekanisme yang menjadi akar sebagian besar bug: word splitting. Ketika BASH menemukan unquoted expansion ($VAR tanpa kutip), ia mengambil nilai variabel, lalu memecahnya berdasarkan karakter dalam IFS menjadi beberapa kata. Setiap kata menjadi argumen terpisah.
IFS (Internal Field Separator) adalah variabel bawaan yang menentukan karakter pemisah — default-nya: spasi, tab, dan newline.
nama="Arman Dwi"
printf '<%s>\n' $nama
printf '<%s>\n' "$nama"<Arman>
<Dwi>
<Arman Dwi>Lihat baris pertama: $nama tanpa kutip dipecah menjadi dua kata Arman dan Dwi, sehingga printf dipanggil dengan dua argumen dan mencetak dua baris. Dengan "$nama", seluruh nilai adalah satu argumen utuh — satu baris.
Inilah mengapa ekspansi tanpa kutip berbahaya: setiap spasi di dalam nilai variabel mengubah struktur argumen. Analoginya: kalian memberi resepsionis satu lembar berisi "Arman Dwi", tapi dia menganggap itu dua orang berbeda.
data="satu,dua,tiga"
IFS=,
echo $data
unset IFS
echo "$data"satu dua tiga
satu,dua,tigaMengubah IFS adalah operasi yang memengaruhi seluruh skrip sesudahnya — itulah mengapa praktik yang aman adalah menyimpannya kembali (unset IFS atau IFS=$'\n' secara lokal di dalam fungsi). Untuk kebutuhan memecah string, mekanisme read -ra atau mapfile jauh lebih aman daripada mengubah IFS global.
$(...) dan BacktickCommand substitution — yang sudah kalian lihat sejak episode 2 ($(date +%A)) — juga tunduk pada aturan quoting. Sintaks modern adalah $(perintah), dan sintaks lama (legacy) adalah backtick `perintah`. Kapan pun memungkinkan, gunakan $(...):
$(echo $(hostname)).echo "Host: $(hostname)" # modern, disarankan
echo "Host: `hostname`" # legacy, hindari
echo "Tanggal: $(date +%Y-%m-%d)"Perhatikan cara memakai hasilnya dengan aman:
path="$(pwd)/file.txt" # aman: hasil pwd digabung tanpa splitting
list="$(ls)" # hasil ls yang berisi spasi tetap satu string
echo "$path"Sama seperti variabel, hasil command substitution yang tidak di-quote juga akan mengalami word splitting. Aturan yang sama berlaku: jika ingin hasilnya utuh, bungkus dengan kutip ganda — "$(perintah)".
Warning
Aturan emas yang akan menyelamatkan kalian dari puluhan bug: selalu kutip ganda ekspansi variabel — kecuali kalian benar-benar ingin word splitting. "$VAR" untuk nilai yang ingin dianggap utuh; $VAR hanya saat kalian sengaja ingin memecahnya (jarang terjadi, dan itu pun ada cara yang lebih aman). BASH tidak punya "kutip pintar" — ia menuruti apa yang kalian tulis.
"$@" Selalu Lebih Aman daripada $@Setiap skrip BASH menerima argumen dalam variabel khusus: $1, $2, $3, dan seterusnya, plus $@ dan $* yang mewakili semua argumen. Perbedaan keduanya dalam hal word splitting sangat menentukan:
$@ — semua argumen, dipecah lagi oleh word splitting (karena unquoted)."$@" — semua argumen, masing-masing tetap utuh sebagai satu entitas terpisah. Inilah perilaku yang selalu kalian inginkan.$* / "$*" — semua argumen digabung menjadi satu string dipisah spasi.#!/bin/bash
for file in $@; do
echo "Mencoba hapus: $file"
rm "$file"
donetouch "Laporan Akhir.txt"
./hapus.sh Laporan Akhir.txtMencoba hapus: Laporan
Mencoba hapus: AkhirSkrip di atas mencoba menghapus satu file bernama Laporan Akhir.txt, tapi karena $@ tidak di-quote, BASH memecahnya menjadi dua argumen: Laporan dan Akhir.txt. Tidak ada file yang berhasil dihapus — dan di dunia nyata, bug seperti ini bisa menghapus file yang salah. Sekarang perbaiki:
#!/bin/bash
for file in "$@"; do
echo "Mencoba hapus: $file"
rm "$file"
done./hapus.sh "Laporan Akhir.txt"
ls Laporan\ Akhir.txt 2>&1 || echo "File berhasil dihapus"Mencoba hapus: Laporan Akhir.txt
File berhasil dihapusSekarang "Laporan Akhir.txt" diperlakukan sebagai satu argumen utuh — persis seperti yang diinginkan. Bedanya "$@" vs $@ adalah beda antara menghapus file yang benar dan menghapus file yang salah. Ini bukan teori: file bernama ber-spasi adalah kenyataan sehari-hari di server produksi.
Tip
Jadikan "$@" sebagai kebiasaan otomatis di setiap skrip yang mem-forward argumen. Pola pasangannya yang juga wajib: "${1:-}" untuk $1 yang bisa kosong, dan "$file" untuk nama file dari variabel. Kutip ganda adalah "sabuk pengaman" BASH — kenakan setiap saat, bukan hanya saat takut.
echo $VAR tanpa kutip. Nilai mengandung spasi → dipecah → output tidak rapi, atau argumen bergeser. Selalu echo "$VAR".
rm $FILE dengan nama ber-spasi. rm menerima dua argumen File dan Akhir.txt alih-alih satu File Akhir.txt. Selalu rm "$FILE".
Mengandalkan ~ di dalam kutip. "~/backup" tidak menjadi home — gunakan "$HOME/backup" atau ~/backup tanpa kutip.
Mencampur "$@" dengan $*. $* menggabungkan semua argumen menjadi satu string — hampir tidak pernah yang kalian inginkan saat mem-forward argumen. Gunakan "$@".
Mengubah IFS tanpa menyimpan kembali. IFS global yang berubah membuat seluruh skrip sesudahnya memecah string secara aneh. Simpan lama, ubah, lalu kembalikan: old_ifs=$IFS; IFS=,; ...; IFS=$old_ifs.
Variabel kosong tanpa kutip. rm $file dengan file="" menjadi rm (tanpa argumen) — atau lebih buruk, rm dengan argumen yang tidak diinginkan. "$file" tetap satu argumen kosong yang aman.
| Kesalahan | Gejala | Solusi |
|---|---|---|
echo $nama | Spasi dalam nilai terpecah jadi beberapa baris/kata | echo "$nama" |
rm $file | File bernama ber-spasi tidak terhapus / file salah terhapus | rm "$file" |
for f in $@ | Argumen ber-spasi dipecah jadi banyak | for f in "$@" |
"~/backup" | Path literal ~/backup, bukan /home/user/backup | "$HOME/backup" |
| IFS diubah tanpa dikembalikan | Word splitting aneh di seluruh skrip | Simpan & kembalikan IFS |
Ada satu pola yang paling sering melengkapi pembahasan quoting: menangani file bernama aneh. Ketika kalian benar-benar harus memproses nama file dengan spasi, karakter aneh, bahkan newline, gunakan null-separated (-print0 + while read -d '') — pola yang aman terhadap semua karakter. Ini akan dibahas mendalam di episode tentang iterasi file, tapi catat istilahnya sejak sekarang.
Caution
BASH tidak memecah string secara otomatis di semua konteks. Word splitting hanya terjadi pada ekspansi yang tidak di-quote — bukan pada string literal. echo a b tetap mencetak a b (dua argumen literal), sedangkan x="a b"; echo $x dipecah. Memahami di mana splitting terjadi dan di mana tidak adalah kunci membaca skrip yang rumit tanpa salah paham.
Pada episode 4 ini kalian telah memahami mekanisme yang menentukan keandalan setiap skrip: tiga jenis quoting ('...' literal, "..." yang mengekspansi variabel tapi melindungi dari splitting, dan \ untuk satu karakter), word splitting dengan IFS sebagai pemecah default, mengapa "$@" adalah bentuk paling aman untuk meneruskan argumen, dan pola ${VAR:-default} yang kembali dipakai untuk melindungi variabel kosong.
Inti yang harus kalian bawa:
"$@" mempertahankan setiap argumen utuh; $@ tanpa kutip merusak argumen ber-spasi.~ tidak diekspansi di dalam kutip; glob tidak diperluas di dalam kutip ganda.Quoting adalah "tata krama" BASH — kecil, mudah diabaikan, dan menentukan apakah skrip kalian sopan (benar) atau kasar (rusak di saat genting). Di episode 5 selanjutnya kita akan memadukan semuanya dengan topik yang membuat BASH terasa ajaib: globbing & pathname expansion — pola *, ?, dan [] untuk mencocokkan banyak file, perbedaan glob di dalam dan di luar kutip yang sudah kalian rasakan hari ini, hingga globstar untuk pencarian bertingkat. Fondasi quoting yang kalian bangun sekarang akan membuat episode itu terasa seperti bermain, bukan bertarung dengan shell. Sampai jumpa!