CLI tool yang baik mengikuti konvensi Unix: argumen posisional, opsi --key=value, opsi singkat -k, dan --help yang informatif. Pada bab ini kita akan membedah perbedaan positional vs flags, membangun parser manual yang menangani kedua format, serta menulis help text yang konsisten dengan standar GNU. Memahami parsing manual penting karena setiap library parsing argumen (kotlinx-cli, picocli, apache-cli-cli) memakai pola yang sama di balik layar.
Positional vs Flags #
Argumen command-line terbagi dua kategori utama. Argumen posisional adalah nilai yang diberikan tanpa nama, urutan menentukan artinya (misalnya cp sumber.txt target.txt). Flag adalah argumen dengan nama yang dimulai - atau -- dan biasanya membawa nilai (misalnya --name=Budi atau --limit 5).
| Jenis | Format | Contoh | Karakteristik |
|---|---|---|---|
| Posisional | nilai | file.txt | Urutan penting, tidak ada nama |
| Flag panjang | --key=value | --name=Budi | Eksplisit, mudah diparse |
| Flag nilai terpisah | --key value | --name Budi | Umum, perlu lookahead |
| Flag singkat | -k value | -n Budi | Ringkas, kombinasi -vn |
| Flag boolean | --verbose | --verbose | Tanpa nilai, kehadiran = true |
Strategi Parsing #
Parser manual memakai loop while dengan indeks dan when untuk klasifikasi. Indeks i dinaikkan manual saat flag memerlukan nilai terpisah (args[++i]). Pola ini menjaga kode tetap eksplisit dan mudah di-debug.
data class Opsi(
var verbose: Boolean = false,
var nama: String = "Tamu",
var limit: Int = 10,
val positional: MutableList<String> = mutableListOf()
)
fun parseArgs(args: Array<String>): Opsi {
val opsi = Opsi()
var i = 0
while (i < args.size) {
val arg = args[i]
when {
arg == "--verbose" || arg == "-v" -> opsi.verbose = true
arg.startsWith("--name=") -> opsi.nama = arg.removePrefix("--name=")
arg == "--name" -> opsi.nama = args[++i]
else -> opsi.positional.add(arg)
}
i++
}
return opsi
}Indeks i dinaikkan satu langkah ekstra saat flag memerlukan nilai terpisah, sehingga nilai tersebut tidak diperlakukan sebagai flag baru di iterasi berikutnya.
Parser Lengkap dengan Validasi #
Cuplikan berikut memperluas kerangka di atas dengan dukungan opsi singkat, validasi nilai yang hilang, dan help text. Program dijalankan dengan argumen demo yang menunjukkan semua skenario:
// Demo parsing argumen command-line manual.
// Mendukung: positional, flag boolean (--verbose), flag key=value (--name=Budi),
// dan opsi singkat (-n Budi). Menampilkan struktur parse dan validasi.
data class Opsi(
var verbose: Boolean = false,
var nama: String = "Tamu",
var limit: Int = 10,
val positional: MutableList<String> = mutableListOf()
)
fun main(args: Array<String>) {
val demoArgs = arrayOf("--verbose", "--name=Budi", "--limit", "5", "file1.txt", "file2.txt")
val opsi = parseArgs(demoArgs)
println("=== Parsing Argumen Command-Line ===")
println("Input: ${demoArgs.joinToString(" ")}")
println()
println("Hasil parse:")
println(" verbose = ${opsi.verbose}")
println(" nama = ${opsi.nama}")
println(" limit = ${opsi.limit}")
println(" positional = ${opsi.positional}")
println()
// Demo skenario error: --limit tanpa nilai
println("=== Skenario error: flag tanpa nilai ===")
val invalidArgs = arrayOf("--limit")
val errOpsi = parseArgs(invalidArgs)
println(" Hasil setelah error: limit=${errOpsi.limit} (default tetap dipakai)")
println()
// Demo help text
println("=== Help Text ===")
printHelp()
}
fun parseArgs(args: Array<String>): Opsi {
val opsi = Opsi()
var i = 0
while (i < args.size) {
val arg = args[i]
when {
arg == "--verbose" || arg == "-v" -> opsi.verbose = true
arg == "--help" || arg == "-h" -> {
printHelp()
return opsi
}
arg.startsWith("--name=") -> opsi.nama = arg.removePrefix("--name=")
arg.startsWith("--limit=") -> {
opsi.limit = arg.removePrefix("--limit=").toIntOrNull() ?: 10
}
arg == "--name" || arg == "-n" -> {
if (i + 1 < args.size) {
opsi.nama = args[++i]
} else {
println("Error: flag $arg memerlukan nilai")
}
}
arg == "--limit" -> {
if (i + 1 < args.size) {
opsi.limit = args[++i].toIntOrNull() ?: 10
} else {
println("Error: flag $arg memerlukan nilai")
}
}
arg.startsWith("--") -> {
println("Error: flag tidak dikenal: $arg")
}
arg.startsWith("-") -> {
println("Error: opsi singkat tidak dikenal: $arg")
}
else -> opsi.positional.add(arg)
}
i++
}
return opsi
}
fun printHelp() {
println("Pemakaian: program [opsi] [file...]")
println()
println("Opsi:")
println(" --verbose, -v Aktifkan output verbose")
println(" --name=NAMA Set nama pengguna (default: Tamu)")
println(" --name NAMA Alternatif: nilai argumen terpisah")
println(" -n NAMA Singkatan untuk --name")
println(" --limit=N Set batas (default: 10)")
println(" --help, -h Tampilkan bantuan ini")
println()
println("Argumen posisional:")
println(" file... Daftar file untuk diproses")
}Output yang Dihasilkan:
=== Parsing Argumen Command-Line ===
Input: --verbose --name=Budi --limit 5 file1.txt file2.txt
Hasil parse:
verbose = true
nama = Budi
limit = 5
positional = [file1.txt, file2.txt]
=== Skenario error: flag tanpa nilai ===
Error: flag --limit memerlukan nilai
Hasil setelah error: limit=10 (default tetap dipakai)
=== Help Text ===
Pemakaian: program [opsi] [file...]
Opsi:
--verbose, -v Aktifkan output verbose
--name=NAMA Set nama pengguna (default: Tamu)
--name NAMA Alternatif: nilai argumen terpisah
-n NAMA Singkatan untuk --name
--limit=N Set batas (default: 10)
--help, -h Tampilkan bantuan ini
Argumen posisional:
file... Daftar file untuk diprosesMari kita bedah bagian-bagian kunci parser di atas:
- Klasifikasi
whenberantai: blokwhenmengevaluasi argumen dari yang paling spesifik (arg == "--verbose") ke yang paling umum (arg.startsWith("--")). Urutan penting karena--name=Budiharus dicocokkan sebelum pengecekanstartsWith("--")menangkapnya sebagai flag tak dikenal. - Lookahead untuk nilai terpisah:
args[++i]mengambil argumen berikutnya dan langsung memajukan indeks, sehingga nilai tersebut tidak diperlakukan sebagai flag baru. Pengecekani + 1 < args.sizemencegahArrayIndexOutOfBoundsExceptionsaat flag muncul di akhir tanpa nilai. - Default aman
?: 10: bilatoIntOrNull()mengembalikannull(input non-numerik), parser memakai default. Pendekatan ini lebih ramah daripada melempar exception, cocok untuk CLI yang ditujukan untuk pengguna akhir. - Branch
elseuntuk posisional: argumen yang tidak diawali-dianggap posisional dan dimasukkan ke list. BranchstartsWith("--")danstartsWith("-")yang tidak tertangkap menampilkan pesan error.
Pola Help Text #
Help text yang baik menjawab tiga pertanyaan: bagaimana memakai program, opsi apa saja yang tersedia, dan apa default-nya. Format GNU/POSIX yang umum dipakai oleh cp --help, ls --help, dan git --help mengikuti pola tiga bagian: Pemakaian, Opsi, dan Argumen posisional.
Pemakaian: program [opsi] [file...]
Opsi:
--verbose, -v Aktifkan output verbose
--name=NAMA Set nama (default: Tamu)
Argumen posisional:
file... Daftar file untuk diprosesTanda kurung siku [ ] berarti opsional, ... berarti berulang. Konvensi ini dipakai seragam di seluruh ekosistem Unix sehingga pengguna langsung memahami tanpa membaca dokumentasi tambahan.
Validasi Lanjutan #
Setelah parsing, lakukan validasi semantik: pastikan limit > 0, file posisional yang diminta benar-benar ada, atau kombinasi flag tidak saling bertentangan. Validasi ini sebaiknya dipisahkan dari parsing agar logika tetap bersih.
fun validasi(opsi: Opsi): List<String> {
val errors = mutableListOf<String>()
if (opsi.limit <= 0) errors.add("limit harus > 0, didapat: ${opsi.limit}")
if (opsi.nama.isBlank()) errors.add("nama tidak boleh kosong")
if (opsi.positional.isEmpty() && !opsi.verbose) {
errors.add("diperlukan minimal satu file atau flag --verbose")
}
return errors
}Bila list error tidak kosong, tampilkan semua sekaligus dan exit dengan kode 1. Pengguna lebih menghargai daftar semua kesalahan daripada harus memperbaiki satu per satu.
Referensi Lanjutan #
- kotlinx.cli
— halaman GitHub
kotlinx-cliuntuk deklarasi argumen, opsi, dan subcommand
Selanjutnya #
CLI sekarang bisa menerima argumen dengan baik. Bab berikutnya membahas pemanfaatan Kotlin untuk otomasi sistem file: rename massal, copy, dan organize by date. Materi tersebut ada di chapter File System Automation.