picocli adalah pustaka CLI (command line interface) berbasis anotasi untuk JVM. Satu kelas beranotasi sudah cukup menjadi aplikasi baris perintah yang lengkap, karena picocli mengurus penguraian argumen, konversi tipe, pesan bantuan, sampai pelengkapan otomatis (auto-completion). Topik ini membangun utilitas baris perintah dengan picocli 4.7.6 yang diunduh lewat @Grab, mencakup kelas perintah pertama, opsi beserta nilai default dan arity, argumen posisi, subperintah (subcommand), kode kembali (exit code), serta pembuatan skrip pelengkapan bash. Seluruh skrip contoh mengoper argumennya secara eksplisit sehingga keluarannya dapat direproduksi apa adanya.
Kelas perintah pertama#
Kelas perintah picocli adalah satu kelas biasa yang dianotasi @Command dan mengimplementasikan Runnable atau Callable. Anotasi @Command menetapkan nama program beserta deskripsinya, sedangkan logikanya ditulis pada metode run() atau call() sesuai antarmuka yang dipilih. Eksekusinya dilakukan lewat CommandLine(perintah).execute(argumen), yaitu metode yang menguraikan argumen, mengonversinya ke tipe setiap field, menjalankan metode tersebut, lalu mengembalikan kode kembali dalam bentuk int. Opsi dianotasi @Option dengan names sebagai bentuk opsi yang sah dan defaultValue sebagai nilai ketika opsi tidak dikirim. Argumen posisi tanpa tanda hubung ditampung field beranotasi @Parameters.
Contoh berikut membangun CLI penghitung kata. Opsi --panjang-min menyaring kata yang lebih pendek dari ambangnya, sedangkan seluruh kata dikirim sebagai argumen posisi.
// hitung-kata.groovy: CLI penghitung kata dengan anotasi picocli
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import java.util.concurrent.Callable
@Command(name = 'hitung-kata', mixinStandardHelpOptions = true,
description = 'Menghitung jumlah kata dan total huruf dari argumen.')
class HitungKata implements Callable<Integer> {
@Option(names = ['--panjang-min'], defaultValue = '0',
description = 'Melewati kata yang lebih pendek dari nilai ini.')
int panjangMin
@Parameters(description = 'Kata-kata yang diperiksa.')
List<String> kata
Integer call() {
def lolos = kata.findAll { it.length() >= panjangMin }
println "Jumlah kata : ${kata.size()}"
println "Kata lolos : ${lolos.size()}"
println "Total huruf : ${lolos*.length().sum()}"
println "Kata terpanjang : ${lolos.max { it.length() }}"
return 0
}
}
int kode = new CommandLine(new HitungKata()).execute('--panjang-min', '3', 'ali', 'budi', 'du', 'citra')
System.exit(kode)Jumlah kata : 4
Kata lolos : 3
Total huruf : 12
Kata terpanjang : citraKata du panjangnya dua sehingga tersaring oleh ambang tiga, dan hitungan sisanya hanya memperhatikan kata yang lolos. Nilai kembalian call() berupa 0 karena data masukannya sah, kemudian System.exit meneruskan nilai itu ke shell sebagai kode kembali prosesnya. Pola argumen eksplisit pada execute dipakai seluruh skrip di topik ini agar keluarannya tetap sama pada setiap eksekusi.
Opsi wajib, arity, dan nilai default#
Anotasi @Option dapat disetel lebih jauh dengan required dan arity, yaitu banyaknya nilai yang boleh ditampung satu kemunculan opsi. Opsi required = true wajib muncul pada argumen, sedangkan arity '1..*' berarti minimal satu nilai tanpa batas atas, sehingga cocok untuk opsi koleksi semacam deret nilai ujian. Seluruh angka setelah --nilai masuk ke List<Integer>, dan konversi String ke Integer berlangsung otomatis. Opsi yang tidak wajib dapat diberi defaultValue agar field tetap terisi meskipun opsinya tidak dikirim. Atribut split melengkapi arity dengan memecah satu teks opsi menjadi banyak nilai, misalnya '85,78,91' menjadi tiga angka.
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.util.concurrent.Callable
@Command(name = 'rekap-nilai', mixinStandardHelpOptions = true,
description = 'Merekap statistik nilai ujian dari beberapa peserta.')
class RekapNilai implements Callable<Integer> {
@Option(names = ['--nilai'], required = true, arity = '1..*',
description = 'Deret nilai ujian, minimal satu angka.')
List<Integer> nilai
@Option(names = ['--judul'], defaultValue = 'Rekap nilai ujian',
description = 'Judul yang dicetak pada baris pertama.')
String judul
Integer call() {
println judul
println "Peserta : ${nilai.size()}"
println "Rata-rata : ${nilai.sum() / nilai.size()}"
println "Tertinggi : ${nilai.max()}"
println "Terendah : ${nilai.min()}"
return 0
}
}
int kode = new CommandLine(new RekapNilai()).execute('--nilai', '85', '78', '91', '70', '88')
System.exit(kode)Rekap nilai ujian
Peserta : 5
Rata-rata : 82.4
Tertinggi : 91
Terendah : 70Lima nilai masuk melalui satu kemunculan --nilai, dan rekapnya dihitung dari daftar itu. Rata-rata 82.4 berasal dari total 412 yang dibagi lima peserta. Baris pertama memakai --judul, dan karena opsinya tidak dikirim, field judul kembali ke nilai defaultnya. Tanpa arity, satu kemunculan opsi hanya menampung satu nilai dan kelebihan argumennya ditolak, seperti dibedah pada bagian Kesalahan umum.
Parameter posisi dan konversi tipe#
Argumen posisi (positional) adalah argumen yang tidak diawali tanda hubung, misalnya nama berkas pada perintah ls nilai.txt. Field beranotasi @Parameters menampungnya secara berurutan, dan atribut index menetapkan urutannya bila ada lebih dari satu field posisi. Konversi tipenya otomatis, yaitu teks masukan diubah ke tipe field tanpa kode tambahan, termasuk ke tipe angka dan File. Field posisi bertipe List menampung seluruh sisa argumen posisi, seperti List<String> kata pada kelas perintah pertama. Bila teks masukan tidak dapat dikonversi ke tipe field, penguraian berhenti sebagai error, bukan berlanjut dengan nilai kosong. Berkat konversi File, perintah yang membaca berkas cukup menerima satu argumen posisi berupa jalur.
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Parameters
@Command(name = 'rekap-berkas', mixinStandardHelpOptions = true,
description = 'Merekap isi sebuah berkas teks.')
class RekapBerkas implements Runnable {
@Parameters(index = '0', description = 'Berkas teks yang direkap.')
File berkas
void run() {
def baris = berkas.readLines('UTF-8').findAll { it.trim() }
println "Baris isi : ${baris.size()}"
println "Karakter : ${berkas.text.length()}"
println "Ukuran byte : ${berkas.length()}"
}
}
def berkas = File.createTempFile('nilai', '.txt')
berkas.deleteOnExit()
berkas.text = '85\n78\n91\n'
int kode = new CommandLine(new RekapBerkas()).execute(berkas.absolutePath)
System.exit(kode)Baris isi : 3
Karakter : 9
Ukuran byte : 9Berkas uji dibuat lewat File.createTempFile di folder sementara, lalu jalurnya dikirim sebagai satu-satunya argumen posisi. picocli mengubah jalur itu menjadi objek File sebelum run() dijalankan, sehingga pembacaan isinya dapat dilakukan langsung. Isinya tiga baris nilai dengan sembilan karakter dan sembilan byte, angka yang saling konsisten dan mudah dihitung ulang.
Subperintah dan keluaran bantuan#
Program dengan banyak fungsi biasanya memilah kemampuannya menjadi subperintah, semacam git yang memiliki git commit dan git push. Pada picocli, kelas induk mendaftarkan kelas-kelas anaknya melalui atribut subcommands pada @Command, lalu execute memilih subperintah berdasarkan argumen pertamanya. Setiap subperintah berdiri sendiri dengan opsi dan logikanya masing-masing, sedangkan bantuan kelas induk mendaftar seluruh subperintah yang tersedia pada bagian Commands. Opsi kelas induk diletakkan sebelum nama subperintah, sedangkan opsi subperintah menyusul setelah namanya. Kelas anak pun dapat mendaftar subcommands lagi di bawahnya, sehingga struktur perintah bertingkat tersusun dari pola yang sama.
// alat-konversi.groovy: subperintah suhu dan jarak beserta keluaran --help
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
@Command(name = 'suhu', description = 'Mengonversi suhu dari Celcius ke Fahrenheit.')
class Suhu implements Runnable {
@Option(names = ['--celcius'], required = true,
description = 'Suhu sumber dalam Celcius.')
double celcius
void run() {
println "suhu : $celcius C = ${celcius * 9 / 5 + 32} F"
}
}
@Command(name = 'jarak', description = 'Mengonversi jarak dari kilometer ke mil.')
class Jarak implements Runnable {
@Option(names = ['--km'], required = true,
description = 'Jarak sumber dalam kilometer.')
double km
void run() {
println "jarak : $km km = ${km * 0.6213712} mil"
}
}
@Command(name = 'alat', mixinStandardHelpOptions = true,
description = 'Kumpulan konversi satuan.', subcommands = [Suhu, Jarak])
class Alat { }
def cmd = new CommandLine(new Alat())
cmd.execute('suhu', '--celcius', '100')
cmd.execute('jarak', '--km', '10')
// keluaran --help dijalankan dua kali, ditangkap, lalu dibandingkan
def tangkap = { Closure aksi ->
def penyangga = new ByteArrayOutputStream()
def keluaranLama = System.out
System.setOut(new PrintStream(penyangga, true, 'UTF-8'))
try {
aksi()
} finally {
System.setOut(keluaranLama)
}
penyangga.toString('UTF-8')
}
def pertama = tangkap { new CommandLine(new Alat()).execute('--help') }
def kedua = tangkap { new CommandLine(new Alat()).execute('--help') }
println "Bantuan identik dua kali: ${pertama == kedua}"
print pertamasuhu : 100.0 C = 212.0 F
jarak : 10.0 km = 6.213712 mil
Bantuan identik dua kali: true
Usage: alat [-hV] [COMMAND]
Kumpulan konversi satuan.
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Commands:
suhu Mengonversi suhu dari Celcius ke Fahrenheit.
jarak Mengonversi jarak dari kilometer ke mil.Subperintah suhu mengonversi 100 Celcius menjadi 212 Fahrenheit, sedangkan jarak mengonversi 10 kilometer menjadi 6.213712 mil. Bagian akhir skrip menjalankan execute('--help') dua kali di dalam penangkap keluaran lalu membandingkan hasilnya, dan pembanding itu mencetak true, artinya transkrip Usage di atas sama persis pada dua eksekusi. Baris Show this help message and exit dan Print version information and exit adalah teks bawaan picocli untuk opsi yang ditambahkan mixinStandardHelpOptions.
Kode kembali#
Metode execute mengembalikan kode kembali yang lazim diperiksa shell untuk memutuskan langkah berikutnya. Nilai kembalian call() pada Callable diteruskan utuh, sehingga keberhasilan ditentukan program sendiri, misalnya 0 untuk sukses dan 1 untuk data yang ditolak. Runnable tidak mengembalikan nilai, dan perintah yang berhasil dijalankan menghasilkan kode 0 secara default, sedangkan nilai bukan nol diteruskan tanpa diubah. Error penguraian diperlakukan berbeda, yaitu picocli mencetak pesannya beserta bantuan ke keluaran stderr lalu execute mengembalikan 2. Exception yang tidak tertangani di dalam run() atau call() dicetak ke stderr dan membuat execute mengembalikan 1. Nilai yang dikembalikan execute selanjutnya diteruskan ke sistem melalui System.exit.
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.util.concurrent.Callable
@Command(name = 'validasi-nilai', description = 'Memeriksa satu nilai ujian.')
class ValidasiNilai implements Callable<Integer> {
@Option(names = ['--nilai'], required = true, description = 'Nilai yang diperiksa.')
int nilai
Integer call() {
if (nilai < 0 || nilai > 100) {
println "Nilai $nilai di luar rentang 0 sampai 100"
return 1
}
println "Nilai $nilai diterima"
return 0
}
}
int kodePertama = new CommandLine(new ValidasiNilai()).execute('--nilai', '85')
int kodeKedua = new CommandLine(new ValidasiNilai()).execute('--nilai', '150')
println "Kode kembali pertama : $kodePertama"
println "Kode kembali kedua : $kodeKedua"Nilai 85 diterima
Nilai 150 di luar rentang 0 sampai 100
Kode kembali pertama : 0
Kode kembali kedua : 1Nilai 85 berada di dalam rentang sehingga diterima dengan kode 0, sedangkan 150 ditolak karena berada di luar rentang, dengan kode 1. Skrip shell yang membungkus CLI ini dapat memercayai kedua kode tersebut, misalnya untuk menghentikan rangkaian perintah ketika datanya ditolak. Aturan yang sama berlaku pada subperintah, karena nilai kembalian subperintah yang dijalankan menjadi nilai kembalian execute.
Pelengkapan otomatis#
picocli menghasilkan skrip pelengkapan otomatis untuk bash dari kelas perintah yang sama, sehingga opsi maupun subperintah baru langsung ikut terlengkapi setelah skripnya dibuat ulang. Generatornya adalah kelas picocli.AutoComplete yang berada di dalam jar inti picocli, bukan modul tambahan. Generator ini membaca anotasi kelas perintah yang sudah terkompilasi, lalu menulis berkas hitung-kata_completion yang berisi fungsi complete untuk seluruh opsinya. Kelas perintah pada skrip Groovy terlebih dahulu dikompilasi dengan groovyc, dan jar picocli untuk classpath dapat disalin dari cache Grape pada folder ~/.groovy/grapes/info.picocli/picocli/jars/.
# tidak-dijalankan
# mengompilasi kelas perintah agar dapat dibaca generator pelengkapan
groovyc -cp picocli-4.7.6.jar hitung-kata.groovy
# menghasilkan berkas pelengkapan hitung-kata_completion pada folder kerja
groovy -cp picocli-4.7.6.jar -e "picocli.AutoComplete.main('HitungKata')"
# memuat pelengkapan pada sesi bash saat ini
source hitung-kata_completionBerkas yang dihasilkan diawali komentar cara pemasangannya, yaitu dimuat lewat source pada .bash_profile atau diletakkan pada folder bash-completion.d. Setelah dimuat, bash melengkapi ketikan opsi ketika tombol Tab ditekan setelah nama perintahnya.
Kesalahan umum#
Dua kegagalan penguraian paling sering terjadi saat @Option bertipe koleksi dipakai tanpa arity dan saat opsi wajib tidak dikirim. Keduanya dilaporkan sebagai ParameterException. Contoh berikut memakai parseArgs, yaitu metode yang hanya menguraikan argumen tanpa menjalankan perintahnya, sehingga exception yang dilemparkannya dapat ditangkap dan pesannya dicetak.
Kesalahan pertama adalah mengirim banyak nilai ke @Option yang arity-nya tidak disetel. Satu kemunculan --nilai hanya menampung satu nilai, sehingga angka kedua dan ketiganya dianggap argumen posisi. Kelas ini tidak memiliki field @Parameters, sehingga picocli menolak sisa argumennya.
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.ParameterException
@Command(name = 'rekap-nilai', description = 'Merekap statistik nilai ujian.')
class RekapNilai implements Runnable {
// tanpa arity: satu kemunculan --nilai hanya menampung satu nilai
@Option(names = ['--nilai'], required = true, description = 'Deret nilai ujian.')
List<Integer> nilai
void run() {
println "Rata-rata: ${nilai.sum() / nilai.size()}"
}
}
def cmd = new CommandLine(new RekapNilai())
try {
cmd.parseArgs('--nilai', '85', '78', '91')
} catch (ParameterException e) {
println "Kelas : ${e.getClass().simpleName}"
println "Pesan : ${e.message}"
}Kelas : UnmatchedArgumentException
Pesan : Unmatched arguments from index 2: '78', '91'Pesan Unmatched arguments from index 2 menunjuk argumen pertama yang tidak tertangani, yaitu 78 pada indeks kedua deret argumen. Menambahkan arity = '1..*' pada anotasinya menyelesaikan kasus ini.
Kesalahan kedua adalah melupakan opsi wajib. Penguraian berhenti sebelum run() sempat dijalankan, dan pesannya menyebut opsi yang hilang beserta bentuk nilainya.
@Grab('info.picocli:picocli:4.7.6')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.ParameterException
@Command(name = 'rekap-nilai', description = 'Merekap statistik nilai ujian.')
class RekapNilai implements Runnable {
@Option(names = ['--nilai'], required = true, arity = '1..*',
description = 'Deret nilai ujian, minimal satu angka.')
List<Integer> nilai
void run() {
println "Rata-rata: ${nilai.sum() / nilai.size()}"
}
}
def cmd = new CommandLine(new RekapNilai())
try {
cmd.parseArgs()
} catch (ParameterException e) {
println "Kelas : ${e.getClass().simpleName}"
println "Pesan : ${e.message}"
}Kelas : MissingParameterException
Pesan : Missing required option: '--nilai=<nilai>'Bila pemanggilannya memakai execute dan bukan parseArgs, kedua kegagalan ini tidak dilempar, melainkan dicetak oleh picocli ke stderr bersama bantuan penggunaan, lalu execute mengembalikan kode 2. Kelas perintah yang tidak mengimplementasikan Runnable maupun Callable juga gagal saat dieksekusi dengan ExecutionException, sehingga salah satu padanannya wajib ada sejak kelas perintah dibuat.
Ringkasan#
- Satu kelas beranotasi @Command yang mengimplementasikan Runnable atau Callable sudah menjadi CLI lengkap, dan
CommandLine(perintah).execute(argumen)menguraikan argumen, mengonversi tipe, menjalankannya, lalu mengembalikan kode kembali. - @Option menampung opsi dengan
names,defaultValue,required, dan arity, sedangkan @Parameters menampung argumen posisi dengan konversi otomatis, termasuk ke tipe File. executemengembalikan nilai kembaliancall(), kode 2 untuk error penguraian, dan 1 untuk exception yang tidak tertangani, laluSystem.exitmeneruskannya ke shell.- Subperintah didaftarkan pada atribut
subcommandskelas induk dan dipilih dari argumen pertama, sedangkan skrip pelengkapan bash dihasilkan olehpicocli.AutoCompletedari jar inti.
Referensi#
- Quick Guide — picocli — panduan resmi singkat tentang kelas perintah beranotasi, @Option, @Parameters, dan pemanggilan execute.
- Dokumentasi picocli — picocli — manual lengkap yang mencakup arity, subcommands, kode kembali, dan bab pelengkapan otomatis.
- Kelas AutoComplete — picocli — dokumentasi API generator skrip pelengkapan bash yang berada di dalam jar inti picocli.