Kotlin cocok untuk membangun CLI tool karena standard library-nya kaya dan sintaksnya ringkas. CLI tool modern seperti git, kubectl, atau gh mengikuti pola yang konsisten: sub-command, argumen posisional, opsi, dan exit code yang terdefinisi. Pada bab ini kita akan merancang struktur CLI tool utuh menggunakan parsing argumen manual, mendefinisikan exit code yang berarti, dan menyediakan bantuan otomatis. Library seperti kotlinx-cli bisa dipakai bila skala CLI sudah besar, namun memahami parsing manual tetap penting agar kita tahu apa yang terjadi di balik layar.

Struktur CLI Tool #

CLI tool Kotlin biasanya terdiri dari tiga lapis: entry point yang menerima args, dispatcher yang memetakan sub-command ke function handler, dan handler yang melakukan pekerjaan sebenarnya. Struktur ini memungkinkan penambahan command baru tanpa mengubah kode yang sudah ada.

kotlin Kerangka dispatcher
fun main(args: Array<String>) {
    val command = args.firstOrNull() ?: "help"
    val params = args.drop(1).toTypedArray()
    when (command) {
        "hello" -> cmdHello(params)
        "add" -> cmdAdd(params)
        "help" -> cmdHelp()
        else -> {
            println("Perintah tidak dikenal: $command")
            kotlin.system.exitProcess(2)
        }
    }
}

args.firstOrNull() menangani skenario CLI dipanggil tanpa argumen. drop(1) memisahkan sub-command dari parameternya. Pemisahan ini membuat handler fokus pada parameter saja.

Exit Code yang Berarti #

Exit code adalah kontrak antara CLI tool dan pemanggil (shell, skrip lain, atau CI). Nilai 0 berarti sukses, nilai selain 0 menandakan kesalahan. Standar umum yang dipakai banyak tool:

CodeArti
0Sukses
1Kesalahan input pengguna
2Perintah tidak dikenal
126File tidak executable
130Dibatalkan oleh Ctrl+C

Bash memakai exit code untuk kontrol alur (if my_cli; then ...), jadi konsistensi sangat penting.

CLI Utility Lengkap #

Cuplikan berikut memodelkan CLI tool utuh dengan tiga sub-command (hello, add, help) dan exit code yang terdefinisi. Saat dipanggil tanpa argumen, program memasuki demo mode yang menjalankan semua command contoh:

kotlin
// Demo CLI sederhana dengan sub-command dan exit code.
// Memodelkan struktur CLI tool seperti git/kubectl: command + args.
// Contoh pemakaian (saat dipanggil tanpa argumen, memakai demo mode):
//   java -jar cli.jar hello Budi
//   java -jar cli.jar add 3 5
//   java -jar cli.jar help

import kotlin.system.exitProcess

fun main(args: Array<String>) {
    if (args.isEmpty()) {
        // Demo mode untuk keperluan tutorial — menampilkan output deterministik
        println("=== Demo Command-Line Utility ===")
        println()
        runCommand(arrayOf("hello", "Budi"))
        println()
        runCommand(arrayOf("add", "3", "5"))
        println()
        runCommand(arrayOf("add", "10", "20", "30"))
        println()
        runCommand(arrayOf("help"))
        println()
        runCommand(arrayOf("tidakada"))
        println("Exit code perintah terakhir: 2")
        return
    }
    val code = runCommand(args)
    exitProcess(code)
}

fun runCommand(args: Array<String>): Int {
    val command = args.firstOrNull() ?: "help"
    val params = args.drop(1).toTypedArray()
    return when (command) {
        "hello" -> cmdHello(params)
        "add" -> cmdAdd(params)
        "help" -> cmdHelp()
        else -> {
            println("Perintah tidak dikenal: $command")
            println("Ketik 'help' untuk daftar perintah yang tersedia.")
            2
        }
    }
}

fun cmdHello(params: Array<String>): Int {
    if (params.isEmpty()) {
        println("Halo, dunia!")
    } else {
        println("Halo, ${params.joinToString(" ")}!")
    }
    return 0
}

fun cmdAdd(params: Array<String>): Int {
    if (params.isEmpty()) {
        println("Error: tidak ada angka untuk dijumlahkan.")
        println("Pemakaian: add <angka1> <angka2> ...")
        return 1
    }
    val angka = params.mapNotNull { it.toIntOrNull() }
    if (angka.size != params.size) {
        val invalid = params.filter { it.toIntOrNull() == null }
        println("Error: input bukan angka: ${invalid.joinToString(", ")}")
        return 1
    }
    println("Input: ${angka.joinToString(" + ")}")
    println("Hasil: ${angka.sum()}")
    return 0
}

fun cmdHelp(): Int {
    println("Pemakaian: cli <perintah> [args]")
    println()
    println("Perintah yang tersedia:")
    println("  hello [nama]    Menyapa pengguna (default: 'dunia')")
    println("  add <a> <b> ...  Menjumlahkan daftar angka")
    println("  help             Menampilkan bantuan ini")
    println()
    println("Exit codes:")
    println("  0  Sukses")
    println("  1  Kesalahan input pengguna")
    println("  2  Perintah tidak dikenal")
    return 0
}

Output yang Dihasilkan:

text
=== Demo Command-Line Utility ===

Halo, Budi!

Input: 3 + 5
Hasil: 8

Input: 10 + 20 + 30
Hasil: 60

Pemakaian: cli <perintah> [args]

Perintah yang tersedia:
  hello [nama]    Menyapa pengguna (default: 'dunia')
  add <a> <b> ...  Menjumlahkan daftar angka
  help             Menampilkan bantuan ini

Exit codes:
  0  Sukses
  1  Kesalahan input pengguna
  2  Perintah tidak dikenal

Perintah tidak dikenal: tidakada
Ketik 'help' untuk daftar perintah yang tersedia.
Exit code perintah terakhir: 2

Mari kita bedah bagian-bagian kunci dari kode di atas:

  1. runCommand dispatcher: function ini menerima seluruh args, mengambil elemen pertama sebagai command, dan drop(1) menghasilkan array parameter. Pemisahan ini membuat handler cmdHello, cmdAdd, dan cmdHelp fokus pada logika bisnis tanpa harus mengurai argumen lagi.
  2. cmdAdd validasi angka: mapNotNull { it.toIntOrNull() } mencoba mengkonversi setiap argumen ke Int. Bila jumlah angka hasil konversi tidak sama dengan jumlah input, berarti ada argumen non-numerik. Validasi ini menjaga agar tidak terjadi NumberFormatException runtime.
  3. Exit code konsisten: setiap handler mengembalikan Int. main() memanggil exitProcess(code) yang meneruskan kode ke shell. 0 artinya sukses, 1 artinya input invalid, 2 artinya command tidak dikenal.
  4. Demo mode: bila args kosong, program menjalankan semua command contoh untuk keperluan tutorial. Pada CLI production, mode ini bisa diganti dengan menampilkan help default.

Menggunakan Library kotlinx-cli #

Untuk CLI berskala besar, library kotlinx-cli mengurangi boilerplate. Kita mendeklarasikan argumen dan opsi sebagai property, lalu library memparse dan memvalidasi.

kotlin Struktur dengan kotlinx-cli
import kotlinx.cli.*

fun main(args: Array<String>) {
    val parser = ArgParser("aplikasi")
    val nama by parser.argument(ArgType.String, description = "Nama pengguna")
    val verbose by parser.option(ArgType.Boolean, fullName = "verbose", shortName = "v").default(false)
    parser.parse(args)
    if (verbose) println("Mode verbose aktif")
    println("Halo, $nama!")
}

Library ini menangani --help, validasi tipe, dan pesan error otomatis. Cocok bila CLI akan tumbuh menjadi 10+ sub-command.

Referensi Lanjutan #

  • Kotlin Command Line — panduan resmi kotlinc dan kotlin CLI untuk menjalankan utilitas baris perintah
  • kotlinx.cli — halaman GitHub kotlinx-cli untuk parsing argumen CLI yang terstruktur

Selanjutnya #

CLI tool sekarang punya struktur. Bab berikutnya membahas parsing argumen secara mendalam: posisional vs flags, format --key=value, dan pola help text yang baik. Materi tersebut ada di chapter Command-Line Argument Parsing.