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).

JenisFormatContohKarakteristik
Posisionalnilaifile.txtUrutan penting, tidak ada nama
Flag panjang--key=value--name=BudiEksplisit, mudah diparse
Flag nilai terpisah--key value--name BudiUmum, perlu lookahead
Flag singkat-k value-n BudiRingkas, kombinasi -vn
Flag boolean--verbose--verboseTanpa 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.

kotlin Kerangka parser manual
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:

kotlin
// 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:

text
=== 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 diproses

Mari kita bedah bagian-bagian kunci parser di atas:

  1. Klasifikasi when berantai: blok when mengevaluasi argumen dari yang paling spesifik (arg == "--verbose") ke yang paling umum (arg.startsWith("--")). Urutan penting karena --name=Budi harus dicocokkan sebelum pengecekan startsWith("--") menangkapnya sebagai flag tak dikenal.
  2. Lookahead untuk nilai terpisah: args[++i] mengambil argumen berikutnya dan langsung memajukan indeks, sehingga nilai tersebut tidak diperlakukan sebagai flag baru. Pengecekan i + 1 < args.size mencegah ArrayIndexOutOfBoundsException saat flag muncul di akhir tanpa nilai.
  3. Default aman ?: 10: bila toIntOrNull() mengembalikan null (input non-numerik), parser memakai default. Pendekatan ini lebih ramah daripada melempar exception, cocok untuk CLI yang ditujukan untuk pengguna akhir.
  4. Branch else untuk posisional: argumen yang tidak diawali - dianggap posisional dan dimasukkan ke list. Branch startsWith("--") dan startsWith("-") 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.

text
Pemakaian: program [opsi] [file...]
Opsi:
  --verbose, -v   Aktifkan output verbose
  --name=NAMA     Set nama (default: Tamu)
Argumen posisional:
  file...         Daftar file untuk diproses

Tanda 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.

kotlin Validasi pasca-parse
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-cli untuk 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.