Skrip yang dipakai orang lain butuh antarmuka: flag, opsi yang butuh nilai, pesan bantuan, dan penanganan input yang sopan. Episode ini membedah getopts sebagai builtin BASH, pola while getopts + case, OPTARG dan OPTIND, fungsi usage(), hingga praktik membangun skrip backup CLI yang lengkap dan aman.

Setelah di episode 20 sebelumnya kita membahas Debugging Techniques — menelusuri eksekusi dengan bash -x, memperkaya jejak dengan PS4, dan menemukan akar bug secara sistematis — kalian kini punya skrip yang tidak hanya bekerja, tetapi bisa diotopsi ketika rusak. Ada satu lompatan terakhir yang memisahkan skrip "yang dipakai sendiri" dari skrip "yang dipakai orang lain": antarmuka.
Bayangkan kalian membagikan skrip backup ke rekan kerja. Pertanyaan pertama yang akan muncul dari mulutnya bukan "bagaimana cara kerjanya?", melainkan "bagaimana cara memakainya?". Jika jawabannya "edit variabel DIR di baris 5", kalian baru saja membuat alat yang harus dipahami dulu sebelum bisa dipakai — dan semua orang akan menghindarinya. Sebaliknya, jika jawabannya ./backup.sh -d /var/www -v, rekan kalian langsung mengerti: ada direktori yang di-backup, ada mode verbose. Tidak perlu membuka file, tidak perlu membaca kode. Antarmuka yang baik adalah dokumentasi yang berjalan.
Inilah pekerjaan getopts: builtin BASH yang mem-parsing opsi baris perintah dengan cara yang standar dan konsisten. Kenapa bukan getopt? Karena getopt adalah program eksternal terpisah dengan banyak varian antar sistem — sedangkan getopts sudah tertanam di dalam BASH sendiri, perilakunya identik di semua sistem, dan lebih sederhana untuk kebutuhan 95% skrip. Di episode ini kita akan membedah sintaks while getopts "d:vh" opt, memahami : untuk opsi yang wajib bernilai, $OPTARG untuk nilai opsi, $OPTIND untuk posisi parsing, menulis fungsi usage(), dan merakit semuanya menjadi skrip backup CLI yang lengkap dengan flag, validasi, dan pesan error yang sopan.
Sejauh ini kalian mungkin sudah terbiasa memakai argumen posisi: $1, $2, $3. Itu bekerja, tetapi punya keterbatasan fatal untuk alat yang dipakai orang lain. Perhatikan dua pemanggilan berikut:
./backup.sh /var/www /tmp/backup-dest verbose
./backup.sh /tmp/backup-dest /var/www verboseKedua baris itu terlihat "sama" tetapi maknanya bisa sangat berbeda — atau justru keduanya salah karena urutan argumen harus dihafal. Sekarang bandingkan dengan gaya flag:
./backup.sh -s /var/www -d /tmp/backup-dest -v
./backup.sh -d /tmp/backup-dest -s /var/www -vFlag menyelesaikan dua masalah sekaligus: urutan tidak lagi penting, dan maksud setiap nilai terbaca dari konteksnya. Inilah alasan hampir semua alat produksi (curl -L, tar -czf, docker build -t) memakai flag. getopts memberikan cara standar untuk membuat flag itu di skrip kalian sendiri, dengan perilaku yang dipahami semua orang yang pernah memakai CLI Linux.
getopts: Optstring, OPTARG, dan OPTINDSintaks inti getopts selalu berbentuk perulangan while dengan case di dalamnya:
#!/bin/bash
while getopts "d:vh" opt; do
case "$opt" in
d)
DEST="$OPTARG"
;;
v)
VERBOSE=true
;;
h)
usage
exit 0
;;
?)
usage
exit 1
;;
esac
doneMari bedah komponennya satu per satu — ini bagian yang paling sering disalahpahami:
| Komponen | Makna |
|---|---|
"d:vh" | Optstring: daftar opsi yang didukung. Titik dua setelah huruf berarti opsi itu wajib diikuti nilai |
$opt | Variabel yang menampung huruf opsi yang sedang diproses (d, v, h, atau ? untuk yang tidak dikenal) |
$OPTARG | Nilai yang menyertai opsi saat ini (diisi hanya untuk opsi yang butuh nilai, seperti -d) |
? | Case penangkap opsi yang tidak dikenal atau opsi bernilai yang kehilangan nilainya |
$OPTIND | Indeks argumen berikutnya yang akan diproses — titik posisi di tengah perulangan |
Optstring "d:vh" dibaca: "-d butuh nilai (karena :), -v dan -h tidak." Perhatikan bahwa tanda titik dua pertama menandai nilai wajib; jika kalian ingin mode error senyap, optstring dimulai dengan titik dua di paling awal (":d:vh") — kita akan bahas perbedaannya di subbab jebakan.
Pola ? di case adalah jaring pengaman: opsi yang tidak kalian kenal (misalnya -z pada optstring di atas) akan masuk ke sini. Di sanalah kalian mencetak usage() dan keluar dengan exit code bukan nol. Dengan pola ini, setiap input yang salah selalu menghasilkan umpan balik yang jelas — ciri alat yang sopan.
shift $((OPTIND - 1)): Memisahkan Flag dari Argumen PosisiSatu detail penting yang sering dilupakan: setelah perulangan getopts selesai, flag sudah habis diproses, tetapi argumen posisi yang tersisa masih ada di $1, $2, dan seterusnya. Nilai OPTIND menunjuk ke argumen pertama yang bukan flag. Untuk menggesernya ke depan, pakai shift:
#!/bin/bash
while getopts "d:v" opt; do
case "$opt" in
d) DEST="$OPTARG" ;;
v) VERBOSE=true ;;
*) usage; exit 1 ;;
esac
done
shift $((OPTIND - 1))
# Mulai dari sini, "$1" dst adalah argumen posisi murni (bukan flag)
echo "Flag diurai. Sisa argumen posisi: $*"Mengapa ini penting? Karena banyak skrip memadukan flag dan argumen posisi — misalnya ./backup.sh -v /var/www /etc/nginx (flag -v, lalu dua path sebagai target). Tanpa shift, kalian tidak akan pernah bisa memisahkan mana flag dan mana target. Baris shift $((OPTIND - 1)) adalah "meja pemisah" yang membuat keduanya tidak saling mengganggu.
Note
OPTIND harus di-reset ke 1 jika kalian memanggil parsing getopts lebih dari sekali dalam sesi shell yang sama (misalnya saat menguji skrip di terminal interaktif dengan source). Tanpa reset, OPTIND terus bertambah dari pemanggilan sebelumnya dan getopts akan melewati argumen yang seharusnya diproses. Ini adalah jebakan nomor satu untuk skrip yang diuji berulang kali — dan masuk daftar pitfall di akhir episode.
usage(): Dokumentasi yang BerjalanSetiap CLI yang layak punya fungsi usage yang menjelaskan cara memakai alat — ditampilkan saat user meminta -h atau ketika input tidak valid. Ini bukan kemewahan; ini ekspektasi dasar setiap pengguna CLI:
#!/bin/bash
usage() {
cat << EOF
Cara pakai: $0 [opsi] <sumber...>
Membuat backup berkompresi dari satu atau lebih direktori.
OPSI:
-d DIR Direktori tujuan backup (default: /backup)
-v Mode verbose, cetak detail proses
-h Tampilkan bantuan ini
CONTOH:
$0 -d /backup -v /var/www
$0 /etc/nginx /etc/ssl
EOF
}Perhatikan detail-detail yang membuatnya profesional:
$0) — bantuan tetap benar walau skrip diganti namanya atau di-symlink.usage mencetak ke stdout saat dipanggil dari -h (permintaan sadar), dan ke stderr saat dipanggil karena kesalahan input. Ini nuansa yang membedakan alat yang diperhatikan dari alat yang asal jadi.while getopts "d:vh" opt; do
case "$opt" in
h) usage; exit 0 ;; # diminta → stdout
?) usage >&2; exit 1 ;; # error → stderr
esac
doneTip
Cetak usage ke stderr ketika input tidak valid (usage >&2) dan ke stdout ketika user meminta bantuan (usage). Alasannya praktis: ketika skrip dipanggil dalam pipeline atau cron, output stdout mungkin diproses lebih lanjut — pesan error kalian tidak boleh tersedot ke dalamnya. Ketika user secara sadar mengetik -h, stdout adalah tempat yang tepat karena dialah yang sedang membaca. Ini detail kecil dengan dampak besar pada kebersihan antarmuka.
Sekarang mari gabungkan semuanya menjadi satu skrip utuh — CLI backup yang profesional: flag untuk direktori tujuan, mode verbose, validasi, dan argumen posisi sebagai daftar sumber:
#!/bin/bash
set -euo pipefail
usage() {
cat << EOF
Cara pakai: $0 [opsi] <sumber...>
Membuat backup berkompresi dari satu atau lebih direktori/file.
OPSI:
-d DIR Direktori tujuan (default: /backup)
-v Mode verbose, cetak detail proses
-h Tampilkan bantuan ini
CONTOH:
$0 -d /backup -v /var/www /etc/nginx
EOF
}
BACKUP_DIR="/backup"
VERBOSE=false
SOURCES=()
while getopts "d:vh" opt; do
case "$opt" in
d) BACKUP_DIR="$OPTARG" ;;
v) VERBOSE=true ;;
h) usage; exit 0 ;;
?) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -eq 0 ]]; then
echo "Error: tidak ada sumber yang diberikan." >&2
usage >&2
exit 1
fi
mkdir -p "$BACKUP_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
ARCHIVE="${BACKUP_DIR}/backup-${STAMP}.tar.gz"
for source in "$@"; do
[[ -e "$source" ]] || { echo "Error: '$source' tidak ditemukan." >&2; exit 1; }
done
if $VERBOSE; then
echo "Tujuan : $BACKUP_DIR"
echo "Arsip : $ARCHIVE"
echo "Sumber : $*"
fi
tar czf "$ARCHIVE" "$@"
echo "Backup selesai: $ARCHIVE"Mari bedah keputusan desain di skrip ini:
set -euo pipefail — semua pelajaran episode 18 diterapkan sejak baris kedua.usage sebagai heredoc terkutip (<< EOF) — pelajaran episode 17: $0 dibiarkan diekspansi karena kita ingin nama skrip dinamis, dan tidak ada $ lain yang tidak diinginkan.BACKUP_DIR="/backup" berarti skrip tetap berguna meski tanpa -d; opsi hanya menimpa default.if [[ $# -eq 0 ]] menolak pemanggilan kosong dengan pesan yang jelas, dan loop [[ -e "$source" ]] memastikan semua sumber ada sebelum tar berjalan (jangan biarkan tar gagal di tengah dan meninggalkan arsip parsial).-v mengubah perilaku, bukan sekadar mencetak — mode verbose menampilkan detail konfigurasi sebelum bekerja; ini contoh flag yang benar-benar berguna, bukan pajangan."$@" untuk seluruh sumber — "$@" mempertahankan setiap argumen sebagai satu string utuh (pelajaran quoting dari episode 4), sehingga path dengan spasi tetap aman.Jalankan dan lihat hasilnya:
./backup-cli.sh -d /backup -v /var/www /etc/nginx
./backup-cli.sh -h
./backup-cli.shTujuan : /backup
Arsip : /backup/backup-20260802-140000.tar.gz
Sumber : /var/www /etc/nginx
Backup selesai: /backup/backup-20260802-140000.tar.gz
Cara pakai: ./backup-cli.sh [opsi] <sumber...>
...
Error: tidak ada sumber yang diberikan.
Cara pakai: ./backup-cli.sh [opsi] <sumber...>Perhatikan bagaimana ketiga pemanggilan memberi respons yang berbeda dan sesuai: pemanggilan normal bekerja, -h menampilkan bantuan dan keluar dengan 0, dan pemanggilan tanpa argumen menolak dengan pesan error di stderr dan exit code 1. Inilah perilaku yang kalian harapkan dari alat produksi — dan sekarang skrip kalian memilikinya.
Important
Dua jebakan yang harus kalian kenali agar CLI kalian tidak meledak: pertama, getopts menangani opsi tergabung (-vh dibaca sebagai -v -h) dan opsi berpisah (-d /backup maupun -d/backup) secara otomatis — ini perilaku bawaan yang bagus, tapi jangan mengandalkannya untuk opsi yang butuh nilai dalam bentuk --dir=/backup (itu gaya long-option yang tidak didukung getopts). Kedua, jika optstring kalian butuh mode senyap (agar getopts tidak mencetak pesan error bawaannya sendiri), mulai optstring dengan titik dua (":d:vh") — lalu $opt menjadi ? untuk opsi tak dikenal dan : untuk opsi yang kehilangan nilainya, dan pesan error sepenuhnya kalian yang tulis.
1. getopts vs getopt. getopts adalah builtin BASH — selalu tersedia, perilaku identik di semua sistem, dan cukup untuk opsi huruf tunggal. getopt adalah program eksternal yang beraneka ragam antar sistem (versi util-linux, versi BSD), dan bahkan pada versi yang sama cara penggunaannya berbeda untuk long option. Untuk skrip yang portabel, gunakan getopts; hanya pertimbangkan getopt saat benar-benar butuh long option (--verbose), dan uji di semua target.
2. Long options tidak didukung. --verbose, --dir=/backup, --help adalah gaya GNU yang getopts tidak kenali — - berlebih membuatnya membaca opsi - lalu karakter berikutnya. Jika kalian ingin long options, kalian harus mem-parsing manual atau beralih ke getopt eksternal. Untuk kebutuhan umum, -v -d /backup -h sudah sangat profesional.
3. OPTIND tidak di-reset. Memanggil getopts dua kali dalam sesi yang sama (saat source skrip di terminal, atau saat menguji) tanpa OPTIND=1 akan membuat parsing berperilaku tak terduga. Selalu set OPTIND=1 sebelum setiap perulangan getopts yang ingin dimulai dari awal.
4. Titik dua hilang di optstring → kegagalan senyap. Jika -d butuh nilai tapi optstring ditulis "dvh" (tanpa titik dua), maka -d /backup akan membaca d sebagai flag tanpa nilai, /backup menjadi argumen posisi, dan tidak ada error yang muncul — nilai $OPTARG tidak pernah terisi. Ini salah satu bug paling menipu; periksa optstring dua kali.
5. Mengabaikan ? dan : di case. Tanpa case ?, opsi yang tidak dikenal akan lolos tanpa pesan — skrip meneruskan dengan asumsi keliru. Selalu sediakan case penangkap (?) yang mencetak usage dan keluar dengan exit code bukan nol. Jika memakai mode senyap (":d:vh"), tambahkan juga case : untuk opsi yang kehilangan nilai.
Caution
Hati-hati saat memakai shift $((OPTIND - 1)) pada skrip yang dijalankan dengan set -u: OPTIND selalu terdefinisi oleh BASH, jadi ini aman. Yang tidak aman adalah mengakses $OPTARG di luar case — nilainya bisa berisi sisa dari opsi sebelumnya. Ambil nilai $OPTARG di dalam case yang relevan, simpan ke variabel (misalnya DEST="$OPTARG"), dan jangan pernah bergantung pada $OPTARG setelah perulangan selesai.
Di episode 21 ini kalian telah mengubah skrip dari sekadar alat pribadi menjadi produk yang bisa dipakai orang lain. Kalian memahami anatomi while getopts "d:vh" opt beserta case, peran : di optstring untuk opsi yang wajib bernilai, $OPTARG sebagai pembawa nilai opsi, $OPTIND sebagai penanda posisi parsing, dan shift $((OPTIND - 1)) untuk memisahkan flag dari argumen posisi. Kalian juga menulis fungsi usage() yang lengkap — dokumentasi yang berjalan — dan merakit semuanya menjadi CLI backup dengan default yang masuk akal, validasi input, mode verbose, dan pesan error yang sopan.
Poin kunci yang harus kalian bawa pulang:
: setelah huruf di optstring menandakan opsi butuh nilai; $OPTARG mengambil nilainya.? dan : di case adalah jaring pengaman — jangan pernah mengabaikannya.OPTIND=1 saat mem-parsing ulang; shift $((OPTIND - 1)) untuk memisahkan flag.usage ke stdout saat diminta -h, ke stderr saat input error; exit code yang benar.Dengan ini, tiga belas episode terakhir telah membentuk fondasi skrip yang lengkap: variabel, alur, fungsi, array, string, input-output, error handling, cleanup, debugging, dan sekarang antarmuka. Di episode 22 selanjutnya kita akan memperluas jangkauan skrip keluar dari mesinnya sendiri dengan topik Integrasi sed, awk, curl & jq — menggabungkan transformasi teks tingkat lanjut, pemanggilan REST API, dan pengolahan JSON dari dalam skrip BASH, sehingga skrip kalian bisa berbicara dengan seluruh ekosistem. Sampai jumpa di episode 22!