Skip to main content

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}