Skip to main content

release_kit/commands/
usage.rs

1//! `rk usage`: the whole command tree in one call.
2//!
3//! Generated from the clap definitions, never hand-maintained, so an
4//! agent loads the surface once instead of walking `--help` per
5//! subcommand and the dump cannot describe a CLI it no longer matches.
6
7use clap::CommandFactory;
8
9use crate::cli::Cli;
10use crate::error::RkError;
11use crate::output::Output;
12
13/// Print every verb, flag, default, and one example each.
14///
15/// # Errors
16///
17/// Never fails; the signature matches the dispatch table.
18pub fn run() -> Result<(), RkError> {
19    let out = Output::human();
20    let root = Cli::command();
21    out.result_line(format!(
22        "rk {} — {}",
23        env!("CARGO_PKG_VERSION"),
24        root.get_about()
25            .map(ToString::to_string)
26            .unwrap_or_default()
27    ));
28    for sub in root.get_subcommands() {
29        describe(out, sub, "rk");
30    }
31    out.next(&[
32        "rk doctor reports whether this host is ready".to_owned(),
33        "rk method --list starts the reading path".to_owned(),
34    ]);
35    Ok(())
36}
37
38/// Print one command's block, then recurse into its subcommands.
39fn describe(out: Output, cmd: &clap::Command, prefix: &str) {
40    let path = format!("{prefix} {}", cmd.get_name());
41    if cmd.has_subcommands() {
42        out.result_line(String::new());
43        out.result_line(format!(
44            "{path} — {}",
45            cmd.get_about().map(ToString::to_string).unwrap_or_default()
46        ));
47        for sub in cmd.get_subcommands() {
48            describe(out, sub, &path);
49        }
50        return;
51    }
52    out.result_line(String::new());
53    out.result_line(format!(
54        "{path} — {}",
55        cmd.get_about().map(ToString::to_string).unwrap_or_default()
56    ));
57    out.result_line(format!("  example: {}", example(cmd, &path)));
58    for arg in cmd.get_arguments() {
59        if matches!(arg.get_id().as_str(), "help" | "version") {
60            continue;
61        }
62        out.result_line(format!("  {}", describe_arg(arg)));
63    }
64}
65
66/// One pasteable example: the command path, every required argument with a
67/// placeholder value, and every required either-or group rendered as its
68/// alternatives — so no example invokes a command in a shape the parser
69/// refuses.
70fn example(cmd: &clap::Command, path: &str) -> String {
71    use std::fmt::Write as _;
72    let mut example = path.to_owned();
73    for arg in cmd.get_arguments() {
74        if !arg.is_required_set() {
75            continue;
76        }
77        let value = format!("<{}>", arg.get_id().as_str().to_ascii_uppercase());
78        match arg.get_long() {
79            Some(long) => {
80                let _ = write!(example, " --{long} {value}");
81            }
82            None => {
83                let _ = write!(example, " {value}");
84            }
85        }
86    }
87    for group in cmd.get_groups() {
88        if !group.is_required_set() {
89            continue;
90        }
91        let alternatives: Vec<String> = group
92            .get_args()
93            .filter_map(|id| {
94                let arg = cmd.get_arguments().find(|arg| arg.get_id() == id)?;
95                Some(arg.get_long().map_or_else(
96                    || arg.get_id().as_str().to_ascii_uppercase(),
97                    |long| format!("--{long}"),
98                ))
99            })
100            .collect();
101        if !alternatives.is_empty() {
102            let _ = write!(example, " <{}>", alternatives.join("|"));
103        }
104    }
105    example
106}
107
108/// One argument line: the form, whether it is required, its help, and its
109/// default where one exists.
110fn describe_arg(arg: &clap::Arg) -> String {
111    use std::fmt::Write as _;
112    let takes_value = arg.get_num_args().is_none_or(|num| num.takes_values());
113    let form = match (arg.get_long(), takes_value) {
114        (Some(long), true) => format!("--{long} <{}>", arg.get_id().as_str().to_ascii_uppercase()),
115        (Some(long), false) => format!("--{long}"),
116        (None, _) => format!("[{}]", arg.get_id().as_str().to_ascii_uppercase()),
117    };
118    let mut line = form;
119    if arg.is_required_set() {
120        line.push_str("  (required)");
121    }
122    if let Some(help) = arg.get_help() {
123        let _ = write!(line, "  {help}");
124    }
125    let defaults = arg.get_default_values();
126    if !defaults.is_empty() {
127        let rendered: Vec<String> = defaults
128            .iter()
129            .map(|v| v.to_string_lossy().into_owned())
130            .collect();
131        let _ = write!(line, " (default: {})", rendered.join(", "));
132    }
133    line
134}