rtb_cli/passthrough.rs
1//! Argument handling for **passthrough** subcommands.
2//!
3//! A command that owns its own clap subtree sets
4//! [`Command::subcommand_passthrough`](rtb_app::command::Command::subcommand_passthrough)
5//! to `true`. The framework's outer parser then captures every token after
6//! `<name>` and stashes it on the [`App`] (see [`App::trailing_args`]);
7//! the command re-parses those tokens with its own [`clap::Parser`].
8//!
9//! [`parse_passthrough`] is the single home for that re-parse — the argv
10//! dance the built-in `docs`/`update` commands would otherwise hand-roll.
11//! Because it reads [`App::trailing_args`] rather than
12//! `std::env::args_os()`, a passthrough command is fully driveable from
13//! [`Application::run_with_args`](crate::Application::run_with_args) in
14//! tests.
15
16use std::ffi::OsString;
17
18use clap::Parser;
19use rtb_app::app::App;
20
21/// Parse a passthrough command's trailing tokens into its clap `Parser`.
22///
23/// Reads [`App::trailing_args`], prepends the command's declared name as a
24/// synthetic `argv[0]` (so `--help`/usage render correctly), and parses.
25///
26/// On `--help`/`--version` the rendered text is printed and the process
27/// exits `0` — matching clap's own [`Parser::parse`] contract. Other parse
28/// failures are returned as a [`miette::Report`] for the caller to `?`.
29///
30/// # Errors
31///
32/// Returns the clap parse error (as a `miette` diagnostic) when the
33/// trailing tokens do not satisfy `T`.
34pub fn parse_passthrough<T: Parser>(app: &App) -> miette::Result<T> {
35 parse_from_trailing::<T>(app.trailing_args())
36}
37
38/// Core of [`parse_passthrough`], split out so it can be unit-tested
39/// without constructing an [`App`].
40fn parse_from_trailing<T: Parser>(trailing: &[OsString]) -> miette::Result<T> {
41 // clap treats `argv[0]` as the program name for help/usage strings;
42 // use the command's declared name so they read as `<name> …`.
43 let name = <T as clap::CommandFactory>::command().get_name().to_owned();
44 let mut argv: Vec<OsString> = Vec::with_capacity(trailing.len() + 1);
45 argv.push(OsString::from(name));
46 argv.extend(trailing.iter().cloned());
47
48 match T::try_parse_from(argv) {
49 Ok(parsed) => Ok(parsed),
50 Err(e) => {
51 use clap::error::ErrorKind;
52 if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
53 // The help/version text is carried on the error; print it
54 // and exit cleanly, exactly as `Parser::parse` would.
55 print!("{e}");
56 std::process::exit(0);
57 }
58 Err(miette::miette!("{e}"))
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[derive(Debug, PartialEq, Eq, Parser)]
68 #[command(name = "demo")]
69 struct Demo {
70 #[arg(long)]
71 region: Option<String>,
72 #[arg(long)]
73 force: bool,
74 }
75
76 #[test]
77 fn parses_trailing_flags() {
78 let args =
79 vec![OsString::from("--region"), OsString::from("eu"), OsString::from("--force")];
80 let parsed: Demo = parse_from_trailing(&args).unwrap();
81 assert_eq!(parsed, Demo { region: Some("eu".into()), force: true });
82 }
83
84 #[test]
85 fn empty_trailing_yields_defaults() {
86 let parsed: Demo = parse_from_trailing(&[]).unwrap();
87 assert_eq!(parsed, Demo { region: None, force: false });
88 }
89
90 #[test]
91 fn unknown_flag_is_an_error_not_a_panic() {
92 let args = vec![OsString::from("--nope")];
93 assert!(parse_from_trailing::<Demo>(&args).is_err());
94 }
95}