Dự án: công cụ dòng lệnh (CLI) với clap trong Rust
Ở trang Đối số dòng lệnh, bạn đã tự tay đọc đối số bằng std::env::args(). Cách đó ổn với một hai đối số. Nhưng công cụ thật cần các tùy chọn như --precision 3, dạng viết tắt như -p 3, một trang --help, thông báo lỗi tốt, và các lệnh con (subcommand) như git commit và git push. Tự viết tất cả những thứ đó là rất nhiều code nhàm chán và dễ sai.
clap (tiếng Anh, Command Line Argument Parser) là crate mà gần như mọi công cụ CLI viết bằng Rust dùng cho việc này. Bạn mô tả các đối số của mình bằng một struct Rust bình thường, và clap viết giúp bạn bộ phân tích (parser), phần trợ giúp và các thông báo lỗi. Trong dự án này, bạn sẽ xây convert, một công cụ đổi đơn vị nhỏ với hai lệnh con: temp cho nhiệt độ và length cho chiều dài.
Code và kết quả trên trang này giữ nguyên tiếng Anh, vì các chú thích /// trở thành chính trang --help mà bạn sẽ thấy bên dưới. Chỉ các chú thích // thường được dịch.
Bạn sẽ xây dựng gì
Phần tiêu đề “Bạn sẽ xây dựng gì”Đây là công cụ hoàn chỉnh trong terminal. Mọi kết quả trên trang này đều đến từ việc chạy chương trình thật.
$ 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'.Mọi thứ sau -- được gửi cho chương trình của bạn thay vì cho Cargo, còn -q (quiet, im lặng) ẩn các dòng “Compiling…” của chính Cargo. Còn thông báo lỗi cuối cùng (giá trị rankine không hợp lệ cho --from, kèm danh sách giá trị được phép)? Ta không hề viết nó. clap viết đấy.
Bước 1: Tạo project và thêm clap
Phần tiêu đề “Bước 1: Tạo project và thêm clap”cargo new convertcd convertcargo add clap --features deriveFeature derive là phần quan trọng. Nó bật #[derive(Parser)], cách dùng clap thân thiện nhất. Giờ Cargo.toml của bạn chứa:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(Số phiên bản của bạn có thể mới hơn một chút. Bất kỳ phiên bản nào bắt đầu bằng 4 đều dùng được cho trang này. Xem Cargo và crate nếu cargo add còn mới với bạn.)
Bước 2: Parser đầu tiên của bạn
Phần tiêu đề “Bước 2: Parser đầu tiên của bạn”Hãy bắt đầu với một công cụ chỉ đổi độ C sang độ F. Bạn viết một struct với mỗi field là một đối số và đặt #[derive(Parser)] lên nó. Thay src/main.rs bằng:
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[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!("(used °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(used °F = °C × 9/5 + 32)clap đọc từng field và quyết định đó là loại đối số gì:
celsiuskhông có tùy chọn#[arg(...)]nào, nên nó là một đối số vị trí (positional argument): giá trị trơn đầu tiên bạn gõ. Vì kiểu của nó làf64, clap còn parse chữ"21.5"thành số giúp bạn.#[arg(short, long)]tạo một tùy chọn (option) với tên ngắn và tên dài lấy từ field:-p/--precision.default_value_t = 2được dùng khi bạn bỏ trống nó.- Một field
booltrở thành một cờ (flag): nó làtruenếu có--verbose(hoặc-v),falsenếu không. - Một field
Option<T>(không dùng ở đây) sẽ là một đối số bạn có thể bỏ hẳn.
{:.*} trong println! nghĩa là “dùng đối số tiếp theo làm số chữ số thập phân”. Đó là cách --precision đến được kết quả in ra.
Cli::parse() đọc dòng lệnh thật. Rust Playground không có dòng lệnh, nên để thử parser ở đó, hãy dùng Cli::parse_from với một danh sách đối số giả vờ. Phần tử đầu tiên luôn là tên chương trình, giống hệt std::env::args():
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[arg(short, long)] verbose: bool,}
fn main() { // Giả vờ người dùng đã gõ: 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);}Kết quả:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FHãy thử đổi các đối số giả vờ: bỏ "--verbose", hoặc đổi "--precision" thành "-p".
Bước 3: Trang trợ giúp từ doc comment
Phần tiêu đề “Bước 3: Trang trợ giúp từ doc comment”Bạn có để ý các dòng /// trong struct không? Đó là các doc comment (chú thích tài liệu), và clap biến chúng thành trang trợ giúp. Chú thích phía trên struct mô tả cả chương trình; chú thích phía trên mỗi field mô tả đối số đó. Bạn có --help (và -h) mà không phải viết thêm gì:
$ cargo run -q -- --helpConvert a temperature from Celsius to Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> The temperature in Celsius
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -v, --verbose Also print the formula that was used -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0Dòng #[command(name = "convert", version = "1.0")] đặt tên và phiên bản hiển thị ở đây. Nếu bạn chỉ viết #[command(version)], clap sẽ lấy phiên bản từ Cargo.toml của bạn.
Các dòng /// của bạn có thể viết bằng tiếng Việt; khi đó trang --help cũng hiện tiếng Việt. Ở đây chúng được giữ tiếng Anh để khớp với kết quả thật ở trên. clap còn giải thích các lỗi giúp bạn:
$ 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'.Lỗi đầu tiên báo thiếu đối số bắt buộc <CELSIUS>; lỗi thứ hai báo abc không phải là một số thực hợp lệ. Trang trợ giúp tốt là món quà cho chính bạn trong tương lai. Hãy viết doc comment như thể có ai đó đang thấy công cụ của bạn lần đầu, vì một ngày nào đó, người đó sẽ là bạn.
Bước 4: Lệnh con
Phần tiêu đề “Bước 4: Lệnh con”Giờ hãy hỗ trợ cả chiều dài. Những công cụ như git và cargo dùng lệnh con (cargo build, cargo test) để nhóm các công việc khác nhau. Trong clap, lệnh con là một enum có #[derive(Subcommand)]. Mỗi biến thể là một lệnh con, và các field của nó là đối số của lệnh con đó:
use clap::{Parser, Subcommand};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convert a temperature Temp { /// The temperature to convert value: f64,
/// The unit to convert from #[arg(short, long)] from: String,
/// The unit to convert to #[arg(short, long)] to: String, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: String,
/// The unit to convert to 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:?}");
// Bạn biết lệnh con nào được dùng bằng một match bình thường. match cli.command { Command::Temp { value, from, to } => println!("temp: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("length: {value} {from} -> {to}"), }}Kết quả:
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 -> miVài chi tiết:
#[command(subcommand)]trên fieldcommandnói với clap rằng “field này chứa lệnh con được chọn”.- Tên biến thể
Temptrở thành lệnh contemp(clap tự đổi thành chữ thường giúp bạn). tempdùng các tùy chọn có tên (--from c --to f) cònlengthdùng đối số vị trí (5 km mi). Cả hai kiểu đều phổ biến; bạn được thấy mỗi kiểu một lần.global = truecho phép--precisionxuất hiện trước hoặc sau lệnh con. Thiếu nó,-p 1chỉ được chấp nhận khi đứng trướclength.
Trang trợ giúp giờ liệt kê các lệnh con, và mỗi lệnh con có trang trợ giúp riêng:
$ cargo run -q -- --helpA small unit converter for temperatures and lengths
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Convert a temperature length Convert a length help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to -h, --help Print helpNhưng có một vấn đề. Các đơn vị chỉ là String trơn, nên clap chấp nhận bất cứ thứ gì:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }Ta sẽ phải tự kiểm tra từng đơn vị về sau. Hãy để clap làm việc đó.
Bước 5: Enum và kiểm tra dữ liệu với ValueEnum
Phần tiêu đề “Bước 5: Enum và kiểm tra dữ liệu với ValueEnum”Khi một đối số chỉ có thể là một trong vài từ, hãy mô tả các từ đó bằng một enum và derive ValueEnum. Khi đó clap chỉ chấp nhận những từ đó, liệt kê chúng trong trang trợ giúp, và từ chối mọi thứ khác. Các enum này thay cho các field String ở Bước 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,}- Mỗi biến thể trở thành một từ viết thường:
Celsius→celsius,Km→km. #[value(alias = "c")]còn chấp nhận dạng ngắnc, mà không hiện nó trong trang trợ giúp.- clap cần
Clone. Ta thêm cảCopy, vì các enum nhỏ này sao chép rất rẻ và điều đó giúpmatchdễ hơn.
Trong enum Command, đổi from: String thành from: TempUnit (và LengthUnit cho chiều dài). Ta cũng thêm hai quy tắc nữa:
/// How many digits to show after the decimal point#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// The temperature to convert#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)chỉ chấp nhận độ chính xác từ 0 đến 6. Nhiều hơn thế chỉ in ra những chữ số vô nghĩa.allow_negative_numbers = truecho phép bạn gõ-40. Thiếu nó, clap nghĩ-40là một tùy chọn ngắn tên-4(xem Lỗi thường gặp). Nhiệt độ có thể âm, nên ta cho phép; chiều dài thì không, nên ta không cho.
Giờ clap bắt được dữ liệu nhập sai trước khi code của bạn kịp chạy:
$ 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'.Và trang trợ giúp giờ hiện các giá trị được phép:
$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpĐây là ý tưởng lớn của clap: khi parse thành công, dữ liệu của bạn đã hợp lệ sẵn. from là một TempUnit thật, không phải một chuỗi có thể là bất cứ thứ gì. Phần còn lại của chương trình có thể tin tưởng nó.
Bước 6: Lỗi và mã thoát
Phần tiêu đề “Bước 6: Lỗi và mã thoát”Có những lỗi clap không bắt được, vì chúng phụ thuộc vào ý nghĩa của giá trị. -500 °C là một con số hoàn toàn hợp lệ, nhưng nó lạnh hơn độ không tuyệt đối (-273.15 °C), nên không phải là một nhiệt độ có thật. Việc kiểm tra đó thuộc về code của chính ta.
Một mẫu gọn gàng là đặt công việc thật vào một hàm run trả về Result, và để main quyết định in gì và trả về mã thoát (exit code) nào. Chương trình báo thành công hay thất bại cho shell bằng một con số: 0 nghĩa là thành công, số khác nghĩa là thất bại. std::process::ExitCode của Rust cho phép main trả về một mã như vậy:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // mã thoát 0 } Err(message) => { eprintln!("error: {message}"); // lỗi được ghi ra stderr ExitCode::FAILURE // mã thoát 1 } }}clap theo đúng những quy tắc đó. Khi parse thất bại, Cli::parse() in lỗi ra stderr và thoát với mã 2, mã thông thường cho “bạn dùng lệnh sai cách”. --help và --version thoát với mã 0. Trong terminal, echo $? hiện mã thoát gần nhất:
$ cargo run -q -- temp -500 -f c -t kerror: -500 °C is colder than absolute zero$ 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 $?2Vì sao điều này quan trọng? Script và các chương trình khác kiểm tra mã thoát để biết công cụ của bạn có chạy thành công không. Trang Biến môi trường và tiến trình nói chi tiết hơn về mã thoát và stderr. Với công cụ lớn hơn, có lẽ bạn sẽ dùng anyhow làm kiểu lỗi cho run thay vì String, để có ngữ cảnh và chuỗi lỗi miễn phí.
Bước 7: Chương trình hoàn chỉnh
Phần tiêu đề “Bước 7: Chương trình hoàn chỉnh”Đây là mọi thứ ghép lại. Thay src/main.rs bằng đoạn này và thử các lệnh ở đầu trang.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[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 { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to 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 { // Một đơn vị này bằng bao nhiêu mét. 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> { // Bước 1: đổi giá trị nhận được, dù là gì, sang độ C. 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} {} is colder than absolute zero", from.symbol())); } // Bước 2: đổi độ C sang đơn vị ta muốn. 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 } }}Hai mẹo nhỏ trong run: {result:.p$} nghĩa là “in result với p chữ số thập phân”, còn dấu ? sau convert_temp(...) trả về sớm kèm lỗi nếu nhiệt độ không thể có thật.
Bước 8: Test parser với try_parse_from
Phần tiêu đề “Bước 8: Test parser với try_parse_from”Cli::parse() thoát chương trình khi có gì sai. Đó là điều bạn muốn cho người dùng, nhưng không phải trong một bài test. Cli::try_parse_from(...) làm cùng việc parse trên một danh sách đối số do bạn chọn, và trả về một Result thay vì thoát. Điều đó khiến nó hoàn hảo để kiểm tra parser, và nó chạy được trên Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[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 { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to 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 cho ta các đối số, tên chương trình đứng đầu. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Chỉ hiện dòng đầu tiên trong thông báo của clap. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Panic nếu chính parser được thiết lập sai (ví dụ, hai // tùy chọn trùng tên ngắn). Một khoản bảo hiểm rẻ. 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");}Kết quả:
$ 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Để ý rằng --version cũng trả về một Err. Với clap, “hiện phiên bản rồi dừng” không phải là một lần parse thành công, nên nó được báo với loại DisplayVersion (và --help với DisplayHelp). Cli::parse() lo những trường hợp đó giúp bạn và thoát với mã 0.
Trong project thật, các phép kiểm tra tương tự được đưa vào bài test. Thêm đoạn này vào cuối 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!("expected temp, got {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.00sLỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Quên feature derive. Nếu bạn viết cargo add clap mà không có --features derive, các derive macro không tồn tại:
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 scopeTrình biên dịch báo rằng nó không tìm thấy derive macro Parser: Parser đã được import, nhưng chỉ là một trait, không có derive macro đi kèm. Cách sửa: chạy cargo add clap --features derive, hoặc tự thêm features = ["derive"] vào Cargo.toml.
Số âm trông giống tùy chọn. Với chương trình ở Bước 2, hãy thử một ngày lạnh giá:
$ 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 thấy dấu - và nghĩ -4 là một tùy chọn ngắn (“unexpected argument”, đối số không mong đợi). Cách sửa: thêm #[arg(allow_negative_numbers = true)] vào field, như ta đã làm ở Bước 5. (Người dùng cũng có thể gõ -- -40; mọi thứ sau một dấu -- đứng riêng được coi là giá trị trơn.)
Gọi Cli::parse() trên Playground. Playground chạy chương trình của bạn mà không có đối số nào, nên parse() in ra một lỗi (hoặc, với công cụ có lệnh con, trang trợ giúp) và thoát với mã 2 trước khi code của bạn kịp làm gì. Ở đó, và trong test, hãy dùng Cli::parse_from([...]) hoặc Cli::try_parse_from([...]).
In lỗi bằng println!. Thông báo lỗi thuộc về stderr (eprintln!), không phải stdout. Nếu ai đó chạy convert ... > result.txt, lỗi vẫn hiện trên màn hình thay vì trốn trong file.
Ý tưởng để mở rộng
Phần tiêu đề “Ý tưởng để mở rộng”- Thêm đơn vị. Thêm yard (
yd) và hải lý vàoLengthUnit. Trình biên dịch sẽ chỉ ra mọimatchbạn cần cập nhật. - Một lệnh con
weightcho kilogam, pound và ounce. - Một cờ
--jsonin kết quả dưới dạng JSON bằng serde_json. - Đọc giá trị từ stdin khi không có giá trị nào được đưa vào, để bạn có thể đưa số vào qua pipe:
echo 100 | convert temp -f c -t f. Một fieldOption<f64>cho bạn biết có giá trị nào được gõ hay không. - Lỗi tốt hơn với anyhow. Đổi
runđể trả vềanyhow::Result<String>và dùnganyhow::bail!. Xem anyhow và thiserror. - Tự hoàn thành trong shell. Crate clap_complete (tiếng Anh) sinh script tự hoàn thành bằng phím Tab cho bash, zsh, fish và PowerShell từ struct
Clicủa bạn.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Thêm clap bằng
cargo add clap --features derive, rồi đặt#[derive(Parser)]lên một struct. Mỗi field là một đối số. - Field trơn là đối số vị trí,
#[arg(short, long)]tạo tùy chọn,booltạo cờ, vàdefault_value_tđặt giá trị mặc định. Doc comment///trở thành nội dung--help. #[derive(Subcommand)]trên một enum cho bạn các lệnh con;#[derive(ValueEnum)]giới hạn một đối số trong một tập từ cố định.value_parser!(...).range(...)kiểm tra khoảng giá trị số.- Sau khi parse thành công, dữ liệu đã hợp lệ sẵn. Đặt các phép kiểm tra còn lại vào một hàm
runtrả vềResult, và trả về mộtExitCodetừmain. - clap thoát với mã 2 khi dùng sai lệnh và mã 0 với
--help/--version. Lỗi của chính bạn cũng nên dùng mã khác 0, và in ra stderr. Cli::try_parse_fromparse một danh sách do bạn chọn và trả về mộtResult, hoàn hảo cho test và cho Playground.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Lệnh cargo add nào bật #[derive(Parser)]?
2. clap lấy mô tả của mỗi tùy chọn trong --help từ đâu?
3. Cli::try_parse_from(["convert", "--version"]) trả về gì?
4. Người dùng gõ một đơn vị không hợp lệ và Cli::parse() thất bại. clap dùng mã thoát nào?
5. Vì sao convert -40 thất bại trong chương trình ở Bước 2?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- clap on docs.rs (tiếng Anh): tài liệu API đầy đủ, với ví dụ cho mọi attribute.
- The clap derive tutorial (tiếng Anh): hướng dẫn chính thức từng bước về đối số, tùy chọn, lệnh con và kiểm tra dữ liệu.
- The clap derive reference (tiếng Anh): giải thích mọi tùy chọn
#[command(...)],#[arg(...)]và#[value(...)]. - Command Line Applications in Rust (tiếng Anh): một cuốn sách miễn phí về xây dựng công cụ CLI thật: kết quả in ra, lỗi, test và đóng gói.
- Exit codes — Command Line Applications in Rust (tiếng Anh): mã thoát có nghĩa là gì và dùng chúng thế nào cho tốt.