Setiap proyek yang terencana dimulai dari struktur yang sederhana tetapi jelas: di mana program berada, bagaimana program dijalankan, dan bagaimana argumen dipilah menjadi perintah. Pada bagian ini kita menyiapkan kerangka tersebut agar fitur pada bagian berikutnya tinggal mengisi tempat yang sudah disiapkan.

Struktur Direktori #

Proyek CLI kita menggunakan satu berkas Kotlin sebagai titik masuk utama, dengan penyimpanan data yang dibuat otomatis saat pertama kali menyimpan:

text
aplikasi-command-line/
└── src/
    ├── tugas.kt         (titik masuk)
    ├── tugas.json       (dibuat otomatis)
    └── build.gradle.kts (konfigurasi build)

Menempatkan skrip di dalam src/ memberi ruang bagi berkas pendukung lain di masa depan tanpa mencampuradukkannya dengan dokumentasi atau berkas build. Berkas tugas.json dibuat otomatis saat perintah tambah pertama kali dijalankan, sehingga pengguna tidak perlu menyiapkan apapun sebelumnya.

Titik Masuk Program #

Program Kotlin CLI dimulai dari function main. Kotlin 2.0 menerima dua bentuk signature: fun main() tanpa parameter, atau fun main(args: Array<String>) ketika argumen command-line diperlukan. Karena kita perlu membaca perintah dari pengguna, kita pakai bentuk kedua:

kotlin
// Bagian 1: Struktur aplikasi dan parsing input command-line.
// Mendemonstrasikan pemetaan perintah menggunakan `when`, kontrak exit code,
// dan pemisahan jalur keluaran normal vs galat.

object KerangkaCli {
    const val EXIT_OK = 0
    const val EXIT_PENGGUNAAN = 1

    fun jalankan(args: Array<String>): Int {
        val perintah = args.firstOrNull() ?: ""
        return when (perintah) {
            "tambah" -> tambah(args.drop(1))
            "daftar" -> daftar()
            "bantuan", "" -> bantuan()
            else -> tidakDikenal(perintah)
        }
    }

    private fun tambah(argumen: List<String>): Int {
        val judul = argumen.firstOrNull()
        if (judul.isNullOrBlank()) {
            System.err.println("Perintah tambah membutuhkan judul tugas.")
            return EXIT_PENGGUNAAN
        }
        println("Tugas \"$judul\" tersimpan dengan id 1.")
        return EXIT_OK
    }

    private fun daftar(): Int {
        println("[ ] 1. Belajar Kotlin dasar")
        return EXIT_OK
    }

    private fun bantuan(): Int {
        println("Penggunaan: tugas <perintah> [argumen]")
        println("  tambah <judul>  menyimpan tugas baru")
        println("  daftar          menampilkan seluruh tugas")
        return EXIT_OK
    }

    private fun tidakDikenal(perintah: String): Int {
        System.err.println("Perintah tidak dikenal: $perintah")
        return EXIT_PENGGUNAAN
    }
}

fun main() {
    val daftarSkenario = listOf(
        arrayOf("tambah", "Belajar Kotlin"),
        arrayOf("daftar"),
        arrayOf("bantuan"),
        arrayOf("tidakada")
    )
    daftarSkenario.forEach { args ->
        println("=== ${args.joinToString(" ")} ===")
        val exit = KerangkaCli.jalankan(args)
        println("Exit: $exit")
    }
}

Output yang Dihasilkan:

text
=== tambah Belajar Kotlin ===
Tugas "Belajar Kotlin" tersimpan dengan id 1.
Exit: 0
=== daftar ===
[ ] 1. Belajar Kotlin dasar
Exit: 0
=== bantuan ===
Penggunaan: tugas <perintah> [argumen]
  tambah <judul>  menyimpan tugas baru
  daftar          menampilkan seluruh tugas
Exit: 0
=== tidakada ===
Exit: 1

Mari kita telaah baris-baris kode penting di atas:

  1. object KerangkaCli: keyword object mendeklarasikan singleton. Seluruh state dan fungsi perintah berada dalam satu wadah yang dapat diakses tanpa membuat instance.
  2. val perintah = args.firstOrNull() ?: "": firstOrNull() mengembalikan null jika array kosong. Operator Elvis ?: memberi string kosong sebagai pengganti agar when tetap mendapat input yang valid.
  3. when (perintah): membandingkan isi perintah terhadap cabang-cabang yang dikenal lalu mengembalikan hasil dari fungsi yang sesuai. Cabang "bantuan", "" menggabungkan dua kasus dalam satu baris.
  4. else -> tidakDikenal(perintah): semua masukan lain jatuh ke cabang default yang menampilkan pesan galat ke STDERR dan mengembalikan exit code 1.
  5. System.err.println(...): pesan galat ditulis ke System.err, bukan println standar, sehingga skrip lain dapat memisahkan keluaran normal dari pesan galat.

Kontrak Exit Code #

Alat CLI yang baik melaporkan hasil eksekusinya melalui exit code, bukan hanya teks. Skrip lain atau CI dapat memeriksa angka ini untuk mengetahui apakah perintah berhasil:

Exit codeArti
0Perintah berhasil dijalankan
1Kesalahan penggunaan, misalnya argumen kurang
2Galat saat membaca atau menulis berkas penyimpanan

Pemisahan STDOUT dan STDERR #

Keluaran normal ditulis ke STDOUT menggunakan println, sedangkan pesan galat ditulis ke STDERR menggunakan System.err.println. Pemisahan dua jalur keluaran ini penting karena pengguna membaca pesan galat di layar, sementara skrip lain dapat menyaring keluaran normal tanpa tercemar pesan galat. Pada shell Unix, pengguna dapat mengalihkan salah satu jalur:

bash
java -jar tugas.jar daftar > daftar.txt        # hanya STDOUT
java -jar tugas.jar tambah 2> galat.txt         # hanya STDERR

Perintah pertama menyimpan hanya daftar tugas ke berkas, sementara perintah kedua menyimpan hanya pesan galat. Tanpa pemisahan ini, skrip harus memilah teks secara manual yang rawan galat.

Mengapa object, Bukan class #

Menggunakan object alih-alih class untuk KerangkaCli memberikan tiga keuntungan praktis. Pertama, seluruh fungsi perintah dapat dipanggil langsung tanpa membuat instance. Kedua, state seperti urutanId (yang akan ditambahkan pada Bagian 3) dapat diakses lintas pemanggilan dalam satu proses. Ketiga, Kotlin menjamin instance singleton dibuat secara thread-safe dan lazy, sehingga tidak perlu inisialisasi manual.

Referensi Lanjutan #

Ringkasan Bagian 1 #

Kerangka proyek telah siap: struktur direktori jelas, titik masuk main memilah perintah melalui when, keluaran normal dan galat terpisah, serta kontrak exit code mendokumentasikan makna setiap nilai. Pada Bagian 2 kita akan menambahkan validasi input yang ketat dan menerjemahkan exception menjadi pesan yang ramah pengguna.