diff options
Diffstat (limited to 'docs/_zh_CN/pages/6-argument-parse-picker.md')
| -rw-r--r-- | docs/_zh_CN/pages/6-argument-parse-picker.md | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md new file mode 100644 index 0000000..9c7e028 --- /dev/null +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -0,0 +1,393 @@ +<h1 align="center">使用 Picker 完成参数解析</h1> +<p align="center"> + 用 Picker 完成基本的参数解析 +</p> + +前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。 + +```rust +@@@ fn main() { +@@@ let args : Vec<String> = vec![]; +let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); +@@@ } +``` + +但是,对于参数较多的场景,这个方案就不够用了:Mingling 提供了 `Picker` —— 通过链式调用来提取和转换参数。 + +要启用 `Picker`,你需要修改 `Cargo.toml` + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["parser"] +``` + +好了,让我们看看 `Picker` 的写法: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or((), "World").unpack(); + ResultName::new(name) +} +``` + +`AsPicker` 为所有可以转换为 `Vec<String>` 的类型实现了 `pick`、`pick_or`、`pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。 + +对于上述示例中的代码: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) -> Next { +let name = prev.pick_or((), "World").unpack(); +@@@ResultName::new(name) +@@@} +``` + +它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or((), "World").unpack(); +// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |______________ 取出第一个位置参数(不指定标志) +// | |______________________ 拾取或使用默认 +// |___________________________ 从前一个输入中 +@@@} +``` + +## 解析标志参数 + +若你的程序需要解析标志参数(例如 `greet --name Alice`),可以使用如下方式 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or(["--name", "-n"], "World").unpack(); + ResultName::new(name) +} +``` + +同理,它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or(["--name", "-n"], "World").unpack(); +// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |____________________________ 取出 "--name" 或 "-n" 后面的参数 +// | |____________________________________ 拾取或使用默认 +// |_________________________________________ 从前一个输入中 +@@@} +``` + +## 关于 `.unpack()` + +你可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。 + +对于只拾取了一次的数据来说,`.unpack()` 会返回单个数据,而对于多次拾取,`Picker` 则会返回元组: + +```rust +// Features: ["parser"] +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultInfo = (String, u8, u32)); + +#[chain] +fn handle_test_entry(prev: EntryTest) -> Next { + let (name, age, id) = prev + .pick::<String>(["--name", "-n"]) + .pick::<u8>(["--age", "-a"]) + .pick::<u32>(["--id", "-I"]) + .unpack(); + + ResultInfo::new((name, age, id)) +} +``` + +> [!IMPORTANT] +> `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的。若你需要解析位置参数,请确保解析前已拾取并消费所有 **标志参数**。 + +## 使用 `pick_or_route` 处理边界情况 + +就像那句老话:"永远不要相信你的用户"。为了应对必要参数缺失、输入类型不匹配等错误情况,`pick_or_route` 能将执行链路由到专门的错误处理类型上。 + +先来看一个简单示例 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or_route(["--name", "-n"], ErrorNoName::default()) + .unpack(); + + // 使用 route! 宏展开 pick_result + let name = route!(pick_result); + ResultName::new(name).into() +} + +#[renderer] +fn render_no_name(_prev: ErrorNoName) { + r_println!("Error: No name provided."); +} + +#[renderer] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); +} +``` + +若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result<Value, Route>`。 + +不过 **Mingling** 的 `extra_macros` 特性提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ use mingling::macros::route; +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = route!(pick_result); +@@@ mingling::macros::empty_result!() +@@@ } +``` + +它展开为: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = match pick_result { + Ok(r) => r, + Err(e) => return e.to_chain(), +}; +@@@ mingling::macros::empty_result!() +@@@ } +``` + +## 提取值的后处理 + +在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev + .pick_or(["--name", "-n"], "World") + // 在提取出 --name 后,立刻格式化 + .after(|name: String| { + name.replace(['-', '_', '.'], " ") + .to_lowercase() + .trim() + .to_string() + }) + .unpack(); + + ResultName::new(name) +} +``` + +同样,你可以使用 `after_or_route` 来处理输入参数的格式错误 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNameTooLong = usize); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or(["--name", "-n"], "World") + .after_or_route(|name: &String| { + if name.len() < 32 { + Ok(name.clone()) + } else { + Err(ErrorNameTooLong::new(name.len())) + } + }) + .unpack(); + let name = route!(pick_result); + + ResultName::new(name).into() +} + +#[renderer] +fn render_name_too_long(prev: ErrorNameTooLong) { + let len = *prev; + r_println!("Error: name too long (length: {} > 32)", len); +} + +#[renderer] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); +} +``` + +## 布尔值解析 + +`Picker` 当然也可以解析布尔类型,但是布尔类型分为显式和隐式模式: + +| 模式 | 格式 | +| ---- | ----------------------------------- | +| 隐式 | `--confirmed` | +| 显式 | `--confirm true` 或 `--confirm yes` | + +- 使用 `.pick::<bool>(flag)` 时,采用隐式解析:只要标志存在即为 `true` +- 使用 `.pick::<Yes>(flag)` 或 `.pick::<True>(flag)` 时,采用显式解析 + +一般来说使用隐式解析即可,但在处理重要的确认行为时,显式逻辑更符合语义。 + +```rust +// Features: ["parser"] +@@@use mingling::parser::Yes; +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); + +#[chain] +fn handle_entry(prev: EntryTest) -> Next { +@@@ let prev1 = prev.clone(); + let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes(); +@@@ let prev = prev1; + let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack(); + ResultDone::default().to_render() +} +``` + +## 特殊用法:`usize` 解析 + +**Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mib` 等字样 + +```rust +// Features: ["parser"] + +#[test] +fn parse_size() { + let vec = vec!["--size".to_string(), "25mib".to_string()]; + let size: usize = vec.pick(["--size", "-S"]).unpack(); + assert_eq!(size, 25 * 1024 * 1024); +} +``` + +## 自定义可解析类型 + +你可以使用 `Pickable` trait 使你的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源 + +```rust +// Features: ["parser"] +@@@use mingling::parser::{Pickable, Argument}; +@@@use mingling::Flag; +#[derive(Default)] +pub struct Address { + ip: String, + port: u16, +} + +impl Pickable for Address { + type Output = Self; + fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> { + let raw = args.pick_argument(flag)?; + let parts: Vec<&str> = raw.split(':').collect(); + let ip = parts.first()?.to_string(); + let port: u16 = parts.get(1)?.parse().ok()?; + Some(Address { ip, port }) + } +} +@@@dispatcher!("connect", CMDConnect => EntryConnect); +@@@pack!(ResultConnected = Address); + +#[chain] +fn handle_connect_entry(prev: EntryConnect) -> Next { + let address: Address = prev.pick("--addr").unpack(); + ResultConnected::new(address) +} + +#[renderer] +fn render_connected(prev: ResultConnected) { + let addr = prev.inner; + r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); +} +``` + +执行效果如下: + +```text +~# my-cli connect --addr 127.0.0.1:8080 +Connected: IP: 127.0.0.1 PORT: 8080 +``` + +## 自动为枚举实现 Pickable + +要为枚举类型实现 `Pickable`,只需该枚举实现了 `EnumTag`,然后为其实现 `PickableEnum` 即可 + +```rust +// Features: ["parser"] +@@@use mingling::parser::PickableEnum; +@@@use mingling::EnumTag; +#[derive(Debug, Default, EnumTag)] +pub enum Fruits { + #[default] + Apple, + Banana, + Orange, +} + +impl PickableEnum for Fruits {} +@@@dispatcher!("eat", CMDEat => EntryEat); +@@@pack!(ResultFruit = Fruits); + +#[chain] +fn handle_eat_entry(prev: EntryEat) -> Next { + let fruit: Fruits = prev.pick("--fruit").unpack(); + ResultFruit::new(fruit) +} + +#[renderer] +fn render_fruit(prev: ResultFruit) { + r_println!("Picked fruit: {:?}", *prev); +} +``` + +以上便是 `Picker` 的所有用法。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> |
