Mengambil keputusan berdasarkan banyak kemungkinan sekaligus dengan `case ... esac`, mencocokkan banyak pola dalam satu blok, dan memvalidasi input seperti email, IP address, serta angka menggunakan regular expression dengan operator `=~`. Dilengkapi praktik membangun CLI command dispatcher dan jebakan yang sering menjebak penulis script.

Setelah di episode 9 sebelumnya kita membahas pengkondisian dasar dengan if, elif, dan else — bagaimana Bash mengambil keputusan berdasarkan exit status sebuah perintah — pada episode kali ini kita akan melangkah ke level berikutnya: mencocokkan satu variabel terhadap banyak pola sekaligus dengan case, serta menguji string menggunakan regular expression dengan operator =~.
Mungkin kalian bertanya-tanya, "Bukankah if saja sudah cukup?" Jawabannya: cukup, tapi jelek. Bayangkan kalian berdiri di depan papan menu restoran yang terdiri dari 50 pertanyaan beruntun: "Apakah kalian mau ayam?" Tidak. "Mau sapi?" Tidak. "Mau ikan?" Tidak... Rasanya membuang waktu, bukan? case adalah cara berpikir yang berbeda: buka daftar menu, langsung tunjuk satu pilihan. Satu variabel, banyak pola, satu blok yang rapi.
Regular expression, di sisi lain, seperti detektor logam di bandara: bukan sekadar mencocokkan nama file di rak (itu kerja glob), melainkan memindai isi string untuk mencari pola karakter di mana pun ia muncul. Dengan =~ di dalam [[ ]], kalian bisa memvalidasi email, alamat IP, atau angka dalam beberapa baris kode. Mari kita bedah keduanya satu per satu.
case?Untuk merasakan masalah yang dipecahkan case, mari lihat dulu versi if yang biasa ditulis pemula:
if [ "$ACTION" = "start" ]; then
echo "Menjalankan service..."
elif [ "$ACTION" = "stop" ]; then
echo "Menghentikan service..."
elif [ "$ACTION" = "restart" ]; then
echo "Me-restart service..."
elif [ "$ACTION" = "status" ]; then
echo "Memeriksa status service..."
else
echo "Aksi tidak dikenal."
fiKode di atas berfungsi, tetapi setiap baris elif mengulang pola yang sama: [ "$ACTION" = "..." ]. Semakin banyak pilihan, semakin panjang rantai, dan semakin besar risiko typo yang membuat satu cabang tidak pernah terpanggil. Di sinilah case unggul: ia memisahkan nilai yang dicocokkan dari pola-pola yang dibandingkan, dan menempatkan semuanya dalam satu struktur yang mudah dipindai mata.
Aturan praktis yang bisa kalian bawa pulang: jika kalian menulis lebih dari dua elif yang membandingkan variabel yang sama, itu pertanda jelas untuk beralih ke case.
case ... esacStruktur case di Bash punya empat bagian utama: kata kunci case, variabel yang diuji, satu blok per pola yang diakhiri ;;, dan penutup esac (yaitu case yang ditulis terbalik — kebiasaan lama Bash/Unix yang juga kita lihat pada if...fi):
case "$VAR" in
pola1)
perintah_untuk_pola1
;;
pola2)
perintah_untuk_pola2
;;
*)
perintah_default
;;
esacMari bedah setiap bagiannya:
case "$VAR" in — variabel yang diuji. Perhatikan kita membungkusnya dengan tanda kutip. Ini penting: variabel kosong atau berisi spasi tetap diperlakukan sebagai satu string, bukan berubah menjadi argumen terpisah atau pola glob.pola1) — pola yang dicocokkan. Jika $VAR cocok dengan pola ini, blok perintah di bawahnya dieksekusi.;; — terminator. Tanda ini memberitahu Bash: "blok pola ini selesai, jangan lanjut memeriksa pola berikutnya." Lupa menulis ;; adalah salah satu kesalahan paling umum dan paling membingungkan karena gejalanya sering aneh.*) — catch-all. Pola wildcard * cocok dengan apa pun, jadi blok ini berfungsi sebagai cabang default, setara dengan else pada if.Analoginya sederhana: case seperti mesin penjual otomatis. Kalian menekan satu tombol ($VAR), mesin mencari tombol itu di daftar pilihan, mengambil produk yang sesuai, lalu ;; adalah suara klik yang menandakan "transaksi selesai, jangan cek tombol lain". Kalau tidak ada tombol yang cocok, sisa koin jatuh ke kotak pengembalian (*).
Important
Setiap blok pola WAJIB diakhiri ;; — kecuali blok terakhir sebelum esac (walaupun sangat disarankan tetap menuliskannya untuk konsistensi). Tanpa ;;, Bash tidak tahu di mana sebuah blok berakhir, dan perintah dari blok sebelumnya bisa ikut tereksekusi ke pola berikutnya. Saat case kalian berperilaku aneh, periksa dulu baris ;; — sembilan dari sepuluh kasus, ini biang keladinya.
|Terkadang beberapa nilai yang berbeda harus diperlakukan sama. Alih-alih menulis dua blok terpisah, Bash mengizinkan kalian menggabungkan pola dengan | (pipe, dibaca "atau"). Nilai id dan id_ID misalnya, keduanya berarti bahasa Indonesia:
case "$LANG" in
id|id_ID)
echo "Halo, selamat datang!"
;;
en|en_US|en_GB)
echo "Hello, welcome!"
;;
*)
echo "Hi!"
;;
esacPola berkelompok ini ibarat jalur bus yang sama melayani beberapa halte: penumpang turun di halte manapun tetap masuk ke satu bus. Semakin banyak alias yang setara, semakin besar manfaatnya — tanpa |, kalian harus menulis satu blok ;; untuk id, satu lagi untuk id_ID, dan seterusnya.
Sekarang kita rangkai semua konsep di atas dalam pola yang paling sering ditemui di dunia nyata: command dispatcher. Pola inilah yang mendasari hampir semua script init, helper systemd, dan tools CLI buatan sendiri. Kalian mengoper nama perintah sebagai argumen pertama, dan case menerjemahkannya menjadi aksi:
#!/usr/bin/env bash
ACTION="${1:-help}"
case "$ACTION" in
start)
echo "Menjalankan service myapp..."
# misal: systemctl start myapp
;;
stop)
echo "Menghentikan service myapp..."
;;
restart)
echo "Me-restart service myapp..."
;;
status)
echo "Status service myapp:"
# misal: systemctl status myapp
;;
help|*)
echo "Penggunaan: $0 {start|stop|restart|status}"
exit 1
;;
esacPerhatikan beberapa detail yang layak ditiru:
ACTION="${1:-help}" — jika tidak ada argumen, $1 kosong dan berubah menjadi help. Ini mencegah case mencocokkan string kosong.help|* sebagai pola terakhir — pola help menangkap permintaan bantuan eksplisit, dan * menangkap semua yang lain. Dengan menggabungkannya, satu blok melayani dua fungsi sekaligus.exit 1 — ketika aksi tidak dikenal, script keluar dengan status failure, bukan diam-diam lanjut ke baris berikutnya. Dalam pipeline atau CI, status keluar inilah yang menentukan sukses atau gagal.casecase tidak hanya mencocokkan string persis — pola yang dipakainya adalah glob pattern yang sama seperti yang kita bahas untuk pathname expansion. Kalian bisa memakai * (apa pun), ? (satu karakter), dan [...] (range karakter). Pola ini sangat berguna untuk mendeteksi tipe file berdasarkan ekstensi:
case "$FILE" in
*.tar.gz|*.tgz)
tar -xzf "$FILE"
;;
*.zip)
unzip "$FILE"
;;
*.png|*.jpg|*.jpeg|*.gif)
echo "$FILE adalah gambar"
;;
*)
echo "Tipe file tidak dikenal: $FILE"
;;
esacPerhatikan pola *.tar.gz: titik ditulis apa adanya (tidak perlu di-escape seperti di regex), karena di glob pattern titik adalah karakter biasa. Ini salah satu perbedaan kunci yang akan kita bahas sebentar lagi.
=~ dalam [[ ]]case adalah alat yang tepat untuk mencocokkan nilai terhadap pola global. Tetapi bagaimana jika kalian perlu menguji bentuk isi sebuah string — misalnya "apakah string ini mengandung setidaknya satu digit", atau "apakah string ini berbentuk alamat email"? Untuk itu, Bash menyediakan operator =~ yang membandingkan string dengan regular expression (regex), hanya tersedia di dalam test majemuk [[ ... ]].
Bedakan keduanya dengan analogi ini:
case dan *) itu seperti menyortir dokumen di rak berdasarkan nama filenya. Polanya mencocokkan seluruh string dari awal sampai akhir.=~) itu seperti memindai isi dokumen dengan detektor. Ia mencari pola karakter di mana pun di dalam string, dan setiap simbol punya makna presisi: . = karakter apa pun, + = satu atau lebih, {2,} = minimal dua, dan seterusnya.Sintaks dasarnya:
if [[ "$input" =~ ^[0-9]+$ ]]; then
echo "$input terdiri dari digit saja."
else
echo "$input mengandung karakter non-digit."
fiKasus paling klasik: memvalidasi apakah sebuah string berbentuk alamat email sebelum script menggunakannya. Regex di bawah memecah email menjadi tiga bagian: nama lokal, @, dan domain:
email="deploy@example.com"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "Email valid: $email"
else
echo "Email tidak valid: $email"
fiMari terjemahkan pola ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ potongan demi potongan:
| Bagian regex | Arti |
|---|---|
^ | Mulai dari awal string |
[A-Za-z0-9._%+-]+ | Satu atau lebih karakter alfanumerik atau simbol lokal |
@ | Karakter @ literal |
[A-Za-z0-9.-]+ | Satu atau lebih karakter pada bagian hostname |
\. | Titik literal (di-escape, karena . biasa berarti "karakter apa pun") |
[A-Za-z]{2,} | Minimal dua huruf (TLD seperti com, id, io) |
$ | Berakhir di akhir string |
Tip
Regex validasi email penuh itu sangat rumit — jauh lebih rumit daripada contoh di atas. Untuk script praktis, validasi format dasar seperti ini sudah cukup mencegah sebagian besar input sampah. Jika kalian benar-benar butuh validasi ketat, cukup kirim email verifikasi dan biarkan server email yang menilai. Jangan terjebak menulis regex 6.000 karakter.
Pola yang sama bisa kita rombak untuk memvalidasi alamat IPv4. Format IPv4 adalah empat blok angka 0–255 yang dipisahkan titik:
ip="192.168.1.10"
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "$ip berformat IPv4."
else
echo "$ip bukan alamat IPv4."
fiBacaan pola ^([0-9]{1,3}\.){3}[0-9]{1,3}$: grup ([0-9]{1,3}\.) berarti "1 sampai 3 digit diikuti titik literal", dan {3} mengulang grup itu tiga kali, lalu diakhiri 1 sampai 3 digit tanpa titik. Perhatikan titik yang di-escape (\.) — inilah perbedaan krusial dengan glob: di regex, . tanpa escape akan cocok dengan karakter apa pun, sehingga 192,168,1,10 pun lolos.
Warning
Regex di atas hanya memvalidasi format, bukan rentang nilai. 999.999.999.999 akan lolos meski bukan IP valid (setiap blok maksimal 255). Untuk validasi penuh, tambahkan pemeriksaan rentang: pecah string dengan IFS='.' read -ra octet <<< "$ip" lalu periksa setiap oktet antara 0–255. Contoh: [[ ${octet[0]} -le 255 ]].
Pola paling sederhana namun paling sering dibutuhkan: memastikan sebuah input adalah angka murni sebelum operasi aritmatika. Ini mencegah error integer expression expected yang misterius saat $input ternyata berisi karakter acak:
read -p "Masukkan umur: " umur
if [[ "$umur" =~ ^[0-9]+$ ]]; then
echo "Umur $umur tahun tercatat."
else
echo "Error: '$umur' bukan angka murni."
fiAda satu jebakan halus yang membedakan pemula dari yang berpengalaman: apa yang terjadi jika pola regex berada di sisi kanan dalam tanda kutip?
# BUKAN regex — string literal yang persis
if [[ "$input" =~ "^[0-9]+$" ]]; then
echo "String literal '^[0-9]+$' cocok"
fi
# Regex sungguhan — pola dievaluasi
if [[ "$input" =~ ^[0-9]+$ ]]; then
echo "$input terdiri dari digit"
fiAturannya sederhana: jika sisi kanan =~ berada dalam tanda kutip (atau diperoleh dari variabel yang ter-quote), itu diperlakukan sebagai string literal — karakter ^, [, +, $ dianggap sebagai karakter biasa, bukan meta-karakter regex. Inilah alasan mengapa banyak orang menulis:
pattern="^[0-9]+$"
if [[ "$input" =~ $pattern ]]; then
echo "Cocok!"
fiVariabel $pattern di atas tidak diberi tanda kutip, sehingga nilainya mengembang menjadi regex yang sesungguhnya dievaluasi. Jika kalian menulis =~ "$pattern", ia menjadi literal. Perilaku ini asimetris dengan sisi kiri — "$input" (sisi kiri) selalu boleh di-quote, sementara sisi kanan harus tanpa kutip jika ingin dievaluasi sebagai regex.
Berikut peta jebakan yang paling sering dialami — simpan sebagai daftar periksa saat script kalian berperilaku aneh:
| Kesalahan | Gejala | Solusi |
|---|---|---|
Lupa ;; di salah satu blok | Perintah dari pola sebelumnya ikut tereksekusi | Pastikan setiap blok diakhiri ;; |
Typo esac (misal esca) | Error syntax error: unexpected end of file | Cek penutup case — harus esac |
case $VAR in tanpa kutip | Variabel berisi glob/spasi cocok dengan pola tak terduga | Tulis case "$VAR" in |
Regex ter-quote di sisi kanan =~ | Pola diperlakukan literal, tidak pernah cocok | Tulis =~ ^pola$ tanpa kutip |
Titik . tanpa escape di regex | . cocok dengan karakter apa pun | Tulis \. untuk titik literal |
Menggunakan = bukan ==/=~ | Perbandingan selalu gagal untuk =~ | Pakai =~ hanya di [[ ]] |
Note
=~ hanya tersedia di dalam [[ ]] (test majemuk bawaan Bash). Ia tidak berfungsi di [ ] (test POSIX) maupun di luar konteks kondisional. Jika kalian mendapatkan error seperti [[: not found atau =~: unary operator expected, itu tandanya kalian memakai bracket yang salah atau script dijalankan dengan sh alih-alih bash.
Pada episode 10 ini kita telah menambah dua senjata penting ke gudang pengkondisian kalian. Dengan case ... esac kalian bisa mencocokkan satu variabel terhadap banyak pola dalam satu blok yang rapi — lengkap dengan pengelompokan pola (pattern1|pattern2), wildcard, dan *) sebagai cabang default — dan kita praktikkan langsung membangun command dispatcher yang menjadi tulang punggung banyak script service. Kemudian dengan =~ di dalam [[ ]] kalian bisa memvalidasi input seperti email, alamat IPv4, dan angka murni menggunakan regular expression, sembari memahami jebakan klasik: regex ter-quote di sisi kanan yang berubah menjadi string literal.
Prinsip yang harus kalian bawa pulang: if untuk keputusan biner, case untuk banyak pilihan, dan =~ untuk menguji bentuk string. Ketiganya adalah dasar dari pengambilan keputusan yang dinamis — dan pengambilan keputusan tanpa kemampuan mengulang seperti memasak nasi tanpa kompor: bisa, tapi belum lengkap.
Di episode 11 selanjutnya kita akan membuka babak baru: perulangan (loops). Kita mulai dengan for — dari bentuk sederhana for item in a b c, rentang {1..10}, hingga C-style for ((i=0; i<10; i++)), lengkap dengan break, continue, dan praktik mass rename file. Di sanalah kalian akan merasakan kekuatan penuh automation. Sampai jumpa di episode berikutnya!