rtb-cli 0.7.1

CLI application scaffolding, clap integration, and built-in commands. Part of the phpboyscout Rust toolkit.
Documentation
//! Argument handling for **passthrough** subcommands.
//!
//! A command that owns its own clap subtree sets
//! [`Command::subcommand_passthrough`](rtb_app::command::Command::subcommand_passthrough)
//! to `true`. The framework's outer parser then captures every token after
//! `<name>` and stashes it on the [`App`] (see [`App::trailing_args`]);
//! the command re-parses those tokens with its own [`clap::Parser`].
//!
//! [`parse_passthrough`] is the single home for that re-parse — the argv
//! dance the built-in `docs`/`update` commands would otherwise hand-roll.
//! Because it reads [`App::trailing_args`] rather than
//! `std::env::args_os()`, a passthrough command is fully driveable from
//! [`Application::run_with_args`](crate::Application::run_with_args) in
//! tests.

use std::ffi::OsString;

use clap::Parser;
use rtb_app::app::App;

/// Parse a passthrough command's trailing tokens into its clap `Parser`.
///
/// Reads [`App::trailing_args`], prepends the command's declared name as a
/// synthetic `argv[0]` (so `--help`/usage render correctly), and parses.
///
/// On `--help`/`--version` the rendered text is printed and the process
/// exits `0` — matching clap's own [`Parser::parse`] contract. Other parse
/// failures are returned as a [`miette::Report`] for the caller to `?`.
///
/// # Errors
///
/// Returns the clap parse error (as a `miette` diagnostic) when the
/// trailing tokens do not satisfy `T`.
pub fn parse_passthrough<T: Parser>(app: &App) -> miette::Result<T> {
    parse_from_trailing::<T>(app.trailing_args())
}

/// Core of [`parse_passthrough`], split out so it can be unit-tested
/// without constructing an [`App`].
fn parse_from_trailing<T: Parser>(trailing: &[OsString]) -> miette::Result<T> {
    // clap treats `argv[0]` as the program name for help/usage strings;
    // use the command's declared name so they read as `<name> …`.
    let name = <T as clap::CommandFactory>::command().get_name().to_owned();
    let mut argv: Vec<OsString> = Vec::with_capacity(trailing.len() + 1);
    argv.push(OsString::from(name));
    argv.extend(trailing.iter().cloned());

    match T::try_parse_from(argv) {
        Ok(parsed) => Ok(parsed),
        Err(e) => {
            use clap::error::ErrorKind;
            if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
                // The help/version text is carried on the error; print it
                // and exit cleanly, exactly as `Parser::parse` would.
                print!("{e}");
                std::process::exit(0);
            }
            Err(miette::miette!("{e}"))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug, PartialEq, Eq, Parser)]
    #[command(name = "demo")]
    struct Demo {
        #[arg(long)]
        region: Option<String>,
        #[arg(long)]
        force: bool,
    }

    #[test]
    fn parses_trailing_flags() {
        let args =
            vec![OsString::from("--region"), OsString::from("eu"), OsString::from("--force")];
        let parsed: Demo = parse_from_trailing(&args).unwrap();
        assert_eq!(parsed, Demo { region: Some("eu".into()), force: true });
    }

    #[test]
    fn empty_trailing_yields_defaults() {
        let parsed: Demo = parse_from_trailing(&[]).unwrap();
        assert_eq!(parsed, Demo { region: None, force: false });
    }

    #[test]
    fn unknown_flag_is_an_error_not_a_panic() {
        let args = vec![OsString::from("--nope")];
        assert!(parse_from_trailing::<Demo>(&args).is_err());
    }
}