Proyek: Membuat CLI dengan clap di Rust
Di halaman Argumen Command-Line kamu membaca argumen secara manual dengan std::env::args(). Cara itu cukup untuk satu atau dua argumen. Tapi alat sungguhan butuh opsi seperti --precision 3, bentuk singkat seperti -p 3, halaman --help, pesan error yang jelas, dan subcommand seperti git commit dan git push. Menulis semua itu sendiri berarti banyak kode yang membosankan dan rawan salah.
clap (Command Line Argument Parser, bahasa Inggris) adalah crate yang dipakai hampir semua CLI Rust untuk urusan ini. Kamu cukup menggambarkan argumen-argumenmu sebagai struct Rust biasa, lalu clap yang menulis parser, teks bantuan, dan pesan error-nya untukmu. Di proyek ini kamu akan membuat convert, pengubah satuan kecil dengan dua subcommand: temp untuk suhu dan length untuk panjang.
Yang akan kamu buat
Section titled “Yang akan kamu buat”Ini alat yang sudah jadi, dijalankan di terminal. Semua output di halaman ini berasal dari menjalankan programnya secara langsung.
$ cargo run -q -- temp 100 --from celsius --to fahrenheit100 °C = 212.00 °F$ cargo run -q -- temp -40 -f c -t f-40 °C = -40.00 °F$ cargo run -q -- length 5 km mi5 km = 3.11 mi$ cargo run -q -- -p 0 length 1 mi ft1 mi = 5280 ft$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.Semua yang ada setelah -- diteruskan ke programmu, bukan ke Cargo, dan -q (quiet) menyembunyikan baris “Compiling…” milik Cargo. Pesan error terakhir itu? Bukan kita yang menulisnya. clap yang membuatnya. Pesan bawaan clap selalu dalam bahasa Inggris; di sini artinya “nilai ‘rankine’ tidak valid untuk --from”, diikuti daftar nilai yang boleh dipakai.
Langkah 1: Buat proyek dan tambahkan clap
Section titled “Langkah 1: Buat proyek dan tambahkan clap”cargo new convertcd convertcargo add clap --features deriveFitur derive adalah bagian terpenting. Fitur ini mengaktifkan #[derive(Parser)], cara paling ramah untuk memakai clap. Sekarang Cargo.toml kamu berisi:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(Nomor versimu mungkin sedikit lebih baru. Versi apa pun yang diawali 4 bisa dipakai untuk halaman ini. Lihat Cargo dan Crate kalau cargo add masih baru buatmu.)
Langkah 2: Parser pertamamu
Section titled “Langkah 2: Parser pertamamu”Kita mulai dengan alat yang hanya mengubah Celsius ke Fahrenheit. Kamu menulis sebuah struct dengan satu field untuk setiap argumen, lalu memasang #[derive(Parser)] di atasnya. Ganti isi src/main.rs dengan:
use clap::Parser;
/// Mengubah suhu dari Celsius ke Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Suhu dalam Celsius celsius: f64,
/// Jumlah angka di belakang koma yang ditampilkan #[arg(short, long, default_value_t = 2)] precision: usize,
/// Cetak juga rumus yang dipakai #[arg(short, long)] verbose: bool,}
fn main() { let cli = Cli::parse(); let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit); if cli.verbose { println!("(rumus: °F = °C × 9/5 + 32)"); }}$ cargo run -q -- 21.521.5 °C = 70.70 °F$ cargo run -q -- 100 --precision 1 --verbose100 °C = 212.0 °F(rumus: °F = °C × 9/5 + 32)clap membaca setiap field dan menentukan jenis argumennya:
celsiustidak punya opsi#[arg(...)], jadi ia menjadi argumen posisional: nilai biasa pertama yang kamu ketik. Karena tipenyaf64, clap juga mengubah teks"21.5"menjadi angka untukmu.#[arg(short, long)]membuat sebuah opsi dengan nama pendek dan nama panjang yang diambil dari nama field:-p/--precision.default_value_t = 2dipakai kalau kamu tidak menuliskannya.- Field
boolmenjadi sebuah flag: nilainyatruekalau--verbose(atau-v) ada, danfalsekalau tidak. - Field
Option<T>(tidak dipakai di sini) akan menjadi argumen yang boleh tidak ditulis sama sekali.
{:.*} di println! artinya “pakai argumen berikutnya sebagai jumlah angka di belakang koma”. Begitulah --precision sampai ke output.
Cli::parse() membaca command line yang asli. Rust Playground tidak punya command line, jadi untuk mencoba parser di sana, pakai Cli::parse_from dengan daftar argumen pura-pura. Item pertama selalu nama program, sama seperti std::env::args():
use clap::Parser;
/// Mengubah suhu dari Celsius ke Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Suhu dalam Celsius celsius: f64,
/// Jumlah angka di belakang koma yang ditampilkan #[arg(short, long, default_value_t = 2)] precision: usize,
/// Cetak juga rumus yang dipakai #[arg(short, long)] verbose: bool,}
fn main() { // Anggap saja pengguna mengetik: convert 100 --precision 1 --verbose let cli = Cli::parse_from(["convert", "100", "--precision", "1", "--verbose"]); println!("{cli:?}");
let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit);}Output:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FCoba ubah argumen pura-puranya: hapus "--verbose", atau ganti "--precision" dengan "-p".
Langkah 3: Teks bantuan dari doc comment
Section titled “Langkah 3: Teks bantuan dari doc comment”Sudah memperhatikan baris-baris /// di struct tadi? Itu adalah doc comment (komentar dokumentasi), dan clap mengubahnya menjadi halaman bantuan. Komentar di atas struct menjelaskan program secara keseluruhan; komentar di atas setiap field menjelaskan argumen itu. Kamu langsung mendapat --help (dan -h) tanpa menulis apa-apa lagi:
$ cargo run -q -- --helpMengubah suhu dari Celsius ke Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> Suhu dalam Celsius
Options: -p, --precision <PRECISION> Jumlah angka di belakang koma yang ditampilkan [default: 2] -v, --verbose Cetak juga rumus yang dipakai -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0Deskripsi yang kita tulis muncul dalam bahasa Indonesia, sedangkan bagian seperti Usage:, Options:, dan Print help adalah teks bawaan clap, jadi tetap dalam bahasa Inggris.
Baris #[command(name = "convert", version = "1.0")] mengatur nama dan versi yang ditampilkan di sini. Kalau kamu hanya menulis #[command(version)], clap mengambil versinya dari Cargo.toml.
clap juga menjelaskan kesalahan untukmu:
$ cargo run -q --error: the following required arguments were not provided: <CELSIUS>
Usage: convert <CELSIUS>
For more information, try '--help'.$ cargo run -q -- abcerror: invalid value 'abc' for '<CELSIUS>': invalid float literal
For more information, try '--help'.Pesan pertama artinya argumen wajib <CELSIUS> tidak diberikan; pesan kedua artinya abc bukan angka desimal yang valid.
Teks bantuan yang bagus adalah hadiah untuk dirimu di masa depan. Tulis doc comment seolah-olah seseorang baru pertama kali melihat alatmu — karena suatu hari nanti, orang itu adalah kamu sendiri.
Langkah 4: Subcommand
Section titled “Langkah 4: Subcommand”Sekarang kita dukung satuan panjang juga. Alat seperti git dan cargo memakai subcommand (cargo build, cargo test) untuk mengelompokkan pekerjaan yang berbeda. Di clap, subcommand adalah sebuah enum dengan #[derive(Subcommand)]. Setiap varian adalah satu subcommand, dan field-field-nya adalah argumen subcommand itu:
use clap::{Parser, Subcommand};
/// Pengubah satuan kecil untuk suhu dan panjang#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Jumlah angka di belakang koma yang ditampilkan #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Ubah satuan suhu Temp { /// Suhu yang mau diubah value: f64,
/// Satuan asal #[arg(short, long)] from: String,
/// Satuan tujuan #[arg(short, long)] to: String, }, /// Ubah satuan panjang Length { /// Panjang yang mau diubah value: f64,
/// Satuan asal from: String,
/// Satuan tujuan to: String, },}
fn main() { let cli = Cli::parse_from(["convert", "temp", "100", "--from", "c", "--to", "f"]); println!("{cli:?}");
let cli = Cli::parse_from(["convert", "length", "5", "km", "mi", "-p", "1"]); println!("{cli:?}");
// Subcommand mana yang dipakai bisa kamu cek dengan match biasa. match cli.command { Command::Temp { value, from, to } => println!("temp: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("length: {value} {from} -> {to}"), }}Output:
Cli { precision: 2, command: Temp { value: 100.0, from: "c", to: "f" } }Cli { precision: 1, command: Length { value: 5.0, from: "km", to: "mi" } }length: 5 km -> miBeberapa detail:
#[command(subcommand)]pada fieldcommandmemberi tahu clap “field ini menyimpan subcommand yang dipilih”.- Nama varian
Tempmenjadi subcommandtemp(clap mengubahnya ke huruf kecil untukmu). tempmemakai opsi bernama (--from c --to f), sedangkanlengthmemakai argumen posisional (5 km mi). Kedua gaya ini sama-sama umum; di sini kamu bisa melihat keduanya.global = truemembuat--precisionboleh ditulis sebelum atau sesudah subcommand. Tanpa itu,-p 1hanya diterima kalau ditulis sebelumlength.
Sekarang halaman bantuan menampilkan daftar subcommand, dan setiap subcommand punya halaman bantuannya sendiri:
$ cargo run -q -- --helpPengubah satuan kecil untuk suhu dan panjang
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Ubah satuan suhu length Ubah satuan panjang help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> Jumlah angka di belakang koma yang ditampilkan [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpUbah satuan suhu
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> Suhu yang mau diubah
Options: -f, --from <FROM> Satuan asal -p, --precision <PRECISION> Jumlah angka di belakang koma yang ditampilkan [default: 2] -t, --to <TO> Satuan tujuan -h, --help Print helpTapi ada masalah. Satuannya berupa String biasa, jadi clap menerima apa saja:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }Kita harus mengecek setiap satuan secara manual nanti. Lebih baik clap saja yang mengerjakannya.
Langkah 5: Enum dan validasi dengan ValueEnum
Section titled “Langkah 5: Enum dan validasi dengan ValueEnum”Kalau sebuah argumen hanya boleh berisi salah satu dari beberapa kata, gambarkan kata-kata itu sebagai enum lalu derive ValueEnum. clap kemudian hanya menerima kata-kata itu, menampilkannya di halaman bantuan, dan menolak yang lain. Enum-enum ini menggantikan field String dari Langkah 4:
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}- Setiap varian menjadi satu kata berhuruf kecil:
Celsius→celsius,Km→km. #[value(alias = "c")]juga menerima bentuk singkatc, tanpa menampilkannya di halaman bantuan.- clap butuh
Clone. Kita tambahkanCopyjuga, karena enum kecil seperti ini murah untuk disalin, dan itu membuatmatchlebih mudah.
Di enum Command, ganti from: String menjadi from: TempUnit (dan LengthUnit untuk panjang). Kita juga menambah dua aturan lagi:
/// Jumlah angka di belakang koma yang ditampilkan#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// Suhu yang mau diubah#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)hanya menerima presisi dari 0 sampai 6. Lebih dari itu cuma mencetak angka yang tidak berguna.allow_negative_numbers = truemembuatmu bisa mengetik-40. Tanpa itu, clap mengira-40adalah opsi pendek bernama-4(lihat Kesalahan umum). Suhu bisa negatif, jadi kita izinkan; panjang tidak bisa, jadi tidak kita izinkan.
Sekarang clap menangkap input yang salah sebelum kodemu sempat berjalan:
$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ cargo run -q -- -p 9 length 1 m cmerror: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6
For more information, try '--help'.Yang terakhir artinya 9 tidak berada di rentang 0..=6.
Dan halaman bantuan sekarang menampilkan nilai-nilai yang boleh dipakai (possible values):
$ cargo run -q -- temp --helpUbah satuan suhu
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> Suhu yang mau diubah
Options: -f, --from <FROM> Satuan asal [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> Jumlah angka di belakang koma yang ditampilkan [default: 2] -t, --to <TO> Satuan tujuan [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpInilah ide besar clap: begitu parsing berhasil, datamu sudah pasti valid. from adalah TempUnit sungguhan, bukan string yang isinya bisa apa saja. Bagian lain programmu bisa langsung memercayainya.
Langkah 6: Error dan exit code
Section titled “Langkah 6: Error dan exit code”Ada kesalahan yang tidak bisa ditangkap clap, karena bergantung pada makna nilainya. -500 °C adalah angka yang sah, tapi lebih dingin dari nol mutlak (-273.15 °C), jadi bukan suhu yang mungkin. Pengecekan seperti itu tempatnya di kode kita sendiri.
Pola yang rapi adalah menaruh pekerjaan utamanya di fungsi run yang mengembalikan Result, lalu membiarkan main memutuskan apa yang dicetak dan exit code (kode keluar) mana yang dikembalikan. Program melaporkan berhasil atau gagal ke shell lewat sebuah angka: 0 artinya berhasil, selain itu artinya gagal. std::process::ExitCode di Rust membuat main bisa mengembalikan angka itu:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // exit code 0 } Err(message) => { eprintln!("error: {message}"); // error dikirim ke stderr ExitCode::FAILURE // exit code 1 } }}clap mengikuti aturan yang sama. Kalau parsing gagal, Cli::parse() mencetak error ke stderr dan keluar dengan kode 2, kode yang umum untuk “cara pakai perintahnya salah”. --help dan --version keluar dengan kode 0. Di terminal, echo $? menampilkan exit code terakhir:
$ cargo run -q -- temp -500 -f c -t kerror: -500 °C lebih dingin dari nol mutlak$ echo $?1$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ echo $?2Kenapa ini penting? Script dan program lain memeriksa exit code untuk tahu apakah alatmu berhasil. Halaman Environment Variable dan Proses membahas exit code dan stderr lebih detail. Di alat yang lebih besar, kamu mungkin akan memakai anyhow sebagai tipe error untuk run alih-alih String, supaya kamu mendapat konteks dan rantai error secara gratis.
Langkah 7: Program lengkap
Section titled “Langkah 7: Program lengkap”Ini semuanya digabung jadi satu. Ganti isi src/main.rs dengan kode ini, lalu coba perintah-perintah di bagian atas halaman.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// Pengubah satuan kecil untuk suhu dan panjang#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Jumlah angka di belakang koma yang ditampilkan #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Ubah satuan suhu Temp { /// Suhu yang mau diubah #[arg(allow_negative_numbers = true)] value: f64,
/// Satuan asal #[arg(short, long)] from: TempUnit,
/// Satuan tujuan #[arg(short, long)] to: TempUnit, }, /// Ubah satuan panjang Length { /// Panjang yang mau diubah value: f64,
/// Satuan asal from: LengthUnit,
/// Satuan tujuan to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
impl TempUnit { fn symbol(self) -> &'static str { match self { TempUnit::Celsius => "°C", TempUnit::Fahrenheit => "°F", TempUnit::Kelvin => "K", } }}
impl LengthUnit { // Berapa meter panjang satu satuan ini. fn meters(self) -> f64 { match self { LengthUnit::Mm => 0.001, LengthUnit::Cm => 0.01, LengthUnit::M => 1.0, LengthUnit::Km => 1000.0, LengthUnit::In => 0.0254, LengthUnit::Ft => 0.3048, LengthUnit::Mi => 1609.344, } }
fn name(self) -> &'static str { match self { LengthUnit::Mm => "mm", LengthUnit::Cm => "cm", LengthUnit::M => "m", LengthUnit::Km => "km", LengthUnit::In => "in", LengthUnit::Ft => "ft", LengthUnit::Mi => "mi", } }}
fn convert_temp(value: f64, from: TempUnit, to: TempUnit) -> Result<f64, String> { // Langkah 1: ubah apa pun yang kita terima menjadi Celsius. let celsius = match from { TempUnit::Celsius => value, TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0, TempUnit::Kelvin => value - 273.15, }; if celsius < -273.15 { return Err(format!("{value} {} lebih dingin dari nol mutlak", from.symbol())); } // Langkah 2: ubah Celsius menjadi satuan yang kita mau. Ok(match to { TempUnit::Celsius => celsius, TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0, TempUnit::Kelvin => celsius + 273.15, })}
fn convert_length(value: f64, from: LengthUnit, to: LengthUnit) -> f64 { value * from.meters() / to.meters()}
fn run(cli: &Cli) -> Result<String, String> { let p = cli.precision as usize; match cli.command { Command::Temp { value, from, to } => { let result = convert_temp(value, from, to)?; Ok(format!("{value} {} = {result:.p$} {}", from.symbol(), to.symbol())) } Command::Length { value, from, to } => { let result = convert_length(value, from, to); Ok(format!("{value} {} = {result:.p$} {}", from.name(), to.name())) } }}
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS } Err(message) => { eprintln!("error: {message}"); ExitCode::FAILURE } }}Ada dua trik kecil di run: {result:.p$} artinya “cetak result dengan p angka di belakang koma”, dan ? setelah convert_temp(...) langsung mengembalikan error kalau suhunya tidak mungkin.
Langkah 8: Menguji parser dengan try_parse_from
Section titled “Langkah 8: Menguji parser dengan try_parse_from”Cli::parse() menghentikan program kalau ada yang salah. Itu yang kamu mau untuk pengguna, tapi tidak untuk test. Cli::try_parse_from(...) melakukan parsing yang sama pada daftar argumen pilihanmu, dan mengembalikan Result alih-alih menghentikan program. Karena itu fungsi ini pas untuk mengecek parser-mu — dan bisa jalan di Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// Pengubah satuan kecil untuk suhu dan panjang#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Jumlah angka di belakang koma yang ditampilkan #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Ubah satuan suhu Temp { /// Suhu yang mau diubah #[arg(allow_negative_numbers = true)] value: f64,
/// Satuan asal #[arg(short, long)] from: TempUnit,
/// Satuan tujuan #[arg(short, long)] to: TempUnit, }, /// Ubah satuan panjang Length { /// Panjang yang mau diubah value: f64,
/// Satuan asal from: LengthUnit,
/// Satuan tujuan to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
fn check(line: &str) { println!("$ {line}"); // split_whitespace memberi kita argumen-argumennya, diawali nama program. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Tampilkan baris pertama pesan clap saja. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Panic kalau parser-nya sendiri salah susun (misalnya dua opsi // dengan nama pendek yang sama). Pengaman yang murah. Cli::command().debug_assert();
check("convert temp -40 -f c -t fahrenheit"); check("convert length 5 km mi -p 1"); check("convert temp 20 --from rankine --to c"); check("convert -p 9 length 1 m cm"); check("convert temp 10 --to c"); check("convert --version");}Output:
$ convert temp -40 -f c -t fahrenheit ok: Cli { precision: 2, command: Temp { value: -40.0, from: Celsius, to: Fahrenheit } }$ convert length 5 km mi -p 1 ok: Cli { precision: 1, command: Length { value: 5.0, from: Km, to: Mi } }$ convert temp 20 --from rankine --to c InvalidValue: error: invalid value 'rankine' for '--from <FROM>'$ convert -p 9 length 1 m cm ValueValidation: error: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6$ convert temp 10 --to c MissingRequiredArgument: error: the following required arguments were not provided:$ convert --version DisplayVersion: convert 1.0Perhatikan bahwa --version juga kembali sebagai Err. Bagi clap, “tampilkan versi lalu berhenti” bukanlah parsing yang berhasil, jadi dilaporkan dengan jenis DisplayVersion (dan --help dengan DisplayHelp). Cli::parse() menangani keduanya untukmu dan keluar dengan kode 0.
Di proyekmu yang sebenarnya, pengecekan yang sama dimasukkan ke dalam test. Tambahkan ini di bagian paling bawah src/main.rs:
#[cfg(test)]mod tests { use super::*; use clap::CommandFactory;
#[test] fn cli_is_set_up_correctly() { Cli::command().debug_assert(); }
#[test] fn parses_a_temperature() { let cli = Cli::try_parse_from(["convert", "temp", "-40", "-f", "c", "-t", "f"]).unwrap(); assert_eq!(cli.precision, 2); match cli.command { Command::Temp { value, from, to } => { assert_eq!(value, -40.0); assert_eq!(from, TempUnit::Celsius); assert_eq!(to, TempUnit::Fahrenheit); } other => panic!("seharusnya temp, tapi yang didapat {other:?}"), } }
#[test] fn rejects_an_unknown_unit() { let result = Cli::try_parse_from(["convert", "length", "3", "yd", "m"]); let err = result.unwrap_err(); assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue); }
#[test] fn minus_forty_is_the_same_in_both() { let f = convert_temp(-40.0, TempUnit::Celsius, TempUnit::Fahrenheit).unwrap(); assert_eq!(f, -40.0); }}$ cargo test -q
running 4 tests....test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sKesalahan umum
Section titled “Kesalahan umum”Lupa fitur derive. Kalau kamu menulis cargo add clap tanpa --features derive, makro derive-nya tidak ada:
error: cannot find derive macro `Parser` in this scope --> src/main.rs:4:10 |4 | #[derive(Parser, Debug)] | ^^^^^^ |note: `Parser` is imported here, but it is only a trait, without a derive macro --> src/main.rs:1:5 |1 | use clap::Parser; | ^^^^^^^^^^^^
error: cannot find attribute `command` in this scopeCompiler bilang makro derive Parser tidak ditemukan: yang ter-import hanya trait-nya, tanpa makro derive. Solusinya: jalankan cargo add clap --features derive, atau tambahkan features = ["derive"] di Cargo.toml secara manual.
Angka negatif terlihat seperti opsi. Dengan program dari Langkah 2, coba hari yang sangat dingin:
$ cargo run -q -- -40error: unexpected argument '-4' found
tip: to pass '-4' as a value, use '-- -4'
Usage: convert [OPTIONS] <CELSIUS>
For more information, try '--help'.clap melihat - dan mengira -4 adalah opsi pendek, jadi ia melaporkan argumen -4 yang tidak dikenal. Solusinya: tambahkan #[arg(allow_negative_numbers = true)] pada field-nya, seperti yang kita lakukan di Langkah 5. (Pengguna juga bisa mengetik -- -40; semua yang ada setelah -- yang berdiri sendiri dianggap nilai biasa.)
Memanggil Cli::parse() di Playground. Playground menjalankan programmu tanpa argumen, jadi parse() mencetak error (atau halaman bantuan, untuk alat yang punya subcommand) dan keluar dengan kode 2 sebelum kodemu sempat melakukan apa pun. Di sana, dan juga di test, pakai Cli::parse_from([...]) atau Cli::try_parse_from([...]).
Mencetak error dengan println!. Pesan error tempatnya di stderr (eprintln!), bukan stdout. Kalau seseorang menjalankan convert ... > hasil.txt, error-nya tetap muncul di layar, bukan tersembunyi di dalam file.
Ide pengembangan
Section titled “Ide pengembangan”- Satuan tambahan. Tambahkan yard (
yd) dan mil laut keLengthUnit. Compiler akan menunjuk setiapmatchyang perlu kamu perbarui. - Subcommand
weightuntuk kilogram, pound, dan ons. - Flag
--jsonyang mencetak hasilnya sebagai JSON dengan serde_json. - Baca nilai dari stdin kalau tidak ada nilai yang diberikan, supaya kamu bisa mengalirkan angka lewat pipe:
echo 100 | convert temp -f c -t f. FieldOption<f64>memberi tahu apakah sebuah nilai diketik atau tidak. - Error yang lebih baik dengan anyhow. Ubah
runsupaya mengembalikananyhow::Result<String>dan pakaianyhow::bail!. Lihat anyhow dan thiserror. - Shell completion. Crate clap_complete (bahasa Inggris) membuat script tab-completion untuk bash, zsh, fish, dan PowerShell dari struct
Cli-mu.
Ringkasan
Section titled “Ringkasan”- Tambahkan clap dengan
cargo add clap --features derive, lalu pasang#[derive(Parser)]pada sebuah struct. Setiap field adalah satu argumen. - Field biasa menjadi argumen posisional,
#[arg(short, long)]membuat opsi,boolmembuat flag, dandefault_value_tmengatur nilai default. Doc comment///menjadi teks--help. #[derive(Subcommand)]pada enum memberimu subcommand;#[derive(ValueEnum)]membatasi argumen pada sekumpulan kata tertentu.value_parser!(...).range(...)mengecek rentang angka.- Setelah parsing berhasil, datanya sudah valid. Taruh pengecekan sisanya di fungsi
runyang mengembalikanResult, dan kembalikanExitCodedarimain. - clap keluar dengan kode 2 untuk kesalahan cara pakai dan 0 untuk
--help/--version. Kegagalan dari kodemu sendiri juga sebaiknya memakai kode selain nol, dan dicetak ke stderr. Cli::try_parse_frommem-parse daftar pilihanmu dan mengembalikanResult— pas untuk test dan Playground.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Perintah cargo add mana yang mengaktifkan #[derive(Parser)]?
2. Dari mana clap mengambil deskripsi setiap opsi di --help?
3. Apa yang dikembalikan Cli::try_parse_from(["convert", "--version"])?
4. Pengguna mengetik satuan yang tidak valid dan Cli::parse() gagal. Exit code berapa yang dipakai clap?
5. Kenapa convert -40 gagal di program Langkah 2?
Skor: 0 / 5
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- clap di docs.rs (bahasa Inggris) — dokumentasi API lengkap, dengan contoh untuk setiap atribut.
- Tutorial derive clap (bahasa Inggris) — tutorial resmi langkah demi langkah tentang argumen, opsi, subcommand, dan validasi.
- Referensi derive clap (bahasa Inggris) — penjelasan setiap opsi
#[command(...)],#[arg(...)], dan#[value(...)]. - Command Line Applications in Rust (bahasa Inggris) — buku gratis tentang membuat alat CLI sungguhan: output, error, testing, dan packaging.
- Exit codes — Command Line Applications in Rust (bahasa Inggris) — arti exit code dan cara memakainya dengan baik.