Lewati ke konten

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.

Ini alat yang sudah jadi, dijalankan di terminal. Semua output di halaman ini berasal dari menjalankan programnya secara langsung.

Terminal window
$ cargo run -q -- temp 100 --from celsius --to fahrenheit
100 °C = 212.00 °F
$ cargo run -q -- temp -40 -f c -t f
-40 °C = -40.00 °F
$ cargo run -q -- length 5 km mi
5 km = 3.11 mi
$ cargo run -q -- -p 0 length 1 mi ft
1 mi = 5280 ft
$ cargo run -q -- temp 20 --from rankine --to c
error: 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.

Terminal window
cargo new convert
cd convert
cargo add clap --features derive

Fitur 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.)

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)");
}
}
Terminal window
$ cargo run -q -- 21.5
21.5 °C = 70.70 °F
$ cargo run -q -- 100 --precision 1 --verbose
100 °C = 212.0 °F
(rumus: °F = °C × 9/5 + 32)

clap membaca setiap field dan menentukan jenis argumennya:

  • celsius tidak punya opsi #[arg(...)], jadi ia menjadi argumen posisional: nilai biasa pertama yang kamu ketik. Karena tipenya f64, 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 = 2 dipakai kalau kamu tidak menuliskannya.
  • Field bool menjadi sebuah flag: nilainya true kalau --verbose (atau -v) ada, dan false kalau 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 °F
▶ Coba sendiri

Coba ubah argumen pura-puranya: hapus "--verbose", atau ganti "--precision" dengan "-p".

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:

Terminal window
$ cargo run -q -- --help
Mengubah 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 -- --version
convert 1.0

Deskripsi 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:

Terminal window
$ cargo run -q --
error: the following required arguments were not provided:
<CELSIUS>
Usage: convert <CELSIUS>
For more information, try '--help'.
$ cargo run -q -- abc
error: 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.

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 -> mi
▶ Coba sendiri

Beberapa detail:

  • #[command(subcommand)] pada field command memberi tahu clap “field ini menyimpan subcommand yang dipilih”.
  • Nama varian Temp menjadi subcommand temp (clap mengubahnya ke huruf kecil untukmu).
  • temp memakai opsi bernama (--from c --to f), sedangkan length memakai argumen posisional (5 km mi). Kedua gaya ini sama-sama umum; di sini kamu bisa melihat keduanya.
  • global = true membuat --precision boleh ditulis sebelum atau sesudah subcommand. Tanpa itu, -p 1 hanya diterima kalau ditulis sebelum length.

Sekarang halaman bantuan menampilkan daftar subcommand, dan setiap subcommand punya halaman bantuannya sendiri:

Terminal window
$ cargo run -q -- --help
Pengubah 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 --help
Ubah 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 help

Tapi ada masalah. Satuannya berupa String biasa, jadi clap menerima apa saja:

Terminal window
$ cargo run -q -- temp 20 --from rankine --to c
Cli { 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 singkat c, tanpa menampilkannya di halaman bantuan.
  • clap butuh Clone. Kita tambahkan Copy juga, karena enum kecil seperti ini murah untuk disalin, dan itu membuat match lebih 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 = true membuatmu bisa mengetik -40. Tanpa itu, clap mengira -40 adalah 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:

Terminal window
$ cargo run -q -- temp 20 --from rankine --to c
error: invalid value 'rankine' for '--from <FROM>'
[possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.
$ cargo run -q -- length 3 yd m
error: 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 cm
error: 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):

Terminal window
$ cargo run -q -- temp --help
Ubah 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 help

Inilah 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.

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:

Terminal window
$ cargo run -q -- temp -500 -f c -t k
error: -500 °C lebih dingin dari nol mutlak
$ echo $?
1
$ cargo run -q -- length 3 yd m
error: invalid value 'yd' for '<FROM>'
[possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.
$ echo $?
2

Kenapa 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.

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.0
▶ Coba sendiri

Perhatikan 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);
}
}
Terminal window
$ cargo test -q
running 4 tests
....
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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 scope

Compiler 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:

Terminal window
$ cargo run -q -- -40
error: 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.

  • Satuan tambahan. Tambahkan yard (yd) dan mil laut ke LengthUnit. Compiler akan menunjuk setiap match yang perlu kamu perbarui.
  • Subcommand weight untuk kilogram, pound, dan ons.
  • Flag --json yang 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. Field Option<f64> memberi tahu apakah sebuah nilai diketik atau tidak.
  • Error yang lebih baik dengan anyhow. Ubah run supaya mengembalikan anyhow::Result<String> dan pakai anyhow::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.
  • 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, bool membuat flag, dan default_value_t mengatur 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 run yang mengembalikan Result, dan kembalikan ExitCode dari main.
  • 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_from mem-parse daftar pilihanmu dan mengembalikan Result — pas untuk test dan Playground.

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