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:
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:
// 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:
=== 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: 1Mari kita telaah baris-baris kode penting di atas:
object KerangkaCli: keywordobjectmendeklarasikan singleton. Seluruh state dan fungsi perintah berada dalam satu wadah yang dapat diakses tanpa membuat instance.val perintah = args.firstOrNull() ?: "":firstOrNull()mengembalikannulljika array kosong. Operator Elvis?:memberi string kosong sebagai pengganti agarwhentetap mendapat input yang valid.when (perintah): membandingkan isiperintahterhadap cabang-cabang yang dikenal lalu mengembalikan hasil dari fungsi yang sesuai. Cabang"bantuan", ""menggabungkan dua kasus dalam satu baris.else -> tidakDikenal(perintah): semua masukan lain jatuh ke cabang default yang menampilkan pesan galat keSTDERRdan mengembalikan exit code1.System.err.println(...): pesan galat ditulis keSystem.err, bukanprintlnstandar, 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 code | Arti |
|---|---|
0 | Perintah berhasil dijalankan |
1 | Kesalahan penggunaan, misalnya argumen kurang |
2 | Galat 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:
java -jar tugas.jar daftar > daftar.txt # hanya STDOUT
java -jar tugas.jar tambah 2> galat.txt # hanya STDERRPerintah 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 #
- Kotlin command-line compiler — rujukan kompilator CLI Kotlin untuk merancang entry point aplikasi command-line
- Kotlin basic syntax
— ringkasan sintaks dasar Kotlin untuk membaca
argsdifun main()
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.