Skip to main content

ctl_core/
usage.rs

1//! Mise Usage spec from a clap [`Command`](clap::Command).
2//!
3//! One hidden flag, one mount line, one operator form. A consumer writes
4//! `--usage-spec[=BIN]` (hidden, `require_equals`). The served mise task
5//! carries [`mount_line`]. Operators then run `mise run q status` — no `--`.
6//! The `--` in the mount is only the completion bootstrap, as mise documents.
7//!
8//! Lefthook calls the same task: `mise run q close-from-git`.
9
10use std::process::ExitCode;
11
12use ::usage::Spec;
13use clap::Command;
14
15use crate::formatdoc;
16
17/// Render a Usage KDL spec for a mise-mounted task named `bin`.
18#[must_use]
19pub fn spec(mut command: Command, bin: &str) -> String {
20    command.set_bin_name(bin);
21    let mut spec = Spec::from(&command);
22    spec.name = bin.to_string();
23    spec.bin = bin.to_string();
24    spec.to_string()
25}
26
27/// The `#USAGE mount` line a served mise file task carries.
28#[must_use]
29pub fn mount_line(task: &str) -> String {
30    formatdoc! {r#"#USAGE mount "mise run --quiet {task} -- --usage-spec={task}""#}
31}
32
33/// If argv contains `--usage-spec[=BIN]`, print the spec for `C` and return
34/// [`ExitCode::SUCCESS`].
35#[must_use]
36pub fn take<C: clap::CommandFactory>(default_bin: &str) -> Option<ExitCode> {
37    let bin = spec_bin(std::env::args().skip(1), default_bin)?;
38    print!("{}", spec(C::command(), &bin));
39    Some(ExitCode::SUCCESS)
40}
41
42/// Parse `--usage-spec` / `--usage-spec=BIN` from argv (after the program
43/// name).
44#[must_use]
45pub fn spec_bin(
46    args: impl IntoIterator<Item = impl AsRef<str>>,
47    default_bin: &str,
48) -> Option<String> {
49    for arg in args {
50        let arg = arg.as_ref();
51        if arg == "--" {
52            break;
53        }
54        if arg == "--usage-spec" {
55            return Some(default_bin.to_owned());
56        }
57        if let Some(bin) = arg.strip_prefix("--usage-spec=") {
58            return Some(if bin.is_empty() {
59                default_bin.to_owned()
60            } else {
61                bin.to_owned()
62            });
63        }
64    }
65    None
66}
67
68#[cfg(test)]
69mod tests {
70    use clap::{CommandFactory, Parser};
71
72    use super::{mount_line, spec, spec_bin};
73
74    #[derive(Parser)]
75    #[command(name = "toy")]
76    struct Toy {
77        #[command(subcommand)]
78        command: ToyCommand,
79    }
80
81    #[derive(clap::Subcommand)]
82    enum ToyCommand {
83        Status,
84        Check,
85    }
86
87    #[test]
88    fn spec_bin_reads_equals_and_bare() {
89        assert_eq!(spec_bin(["--usage-spec"], "qctl").as_deref(), Some("qctl"));
90        assert_eq!(spec_bin(["--usage-spec=q"], "qctl").as_deref(), Some("q"));
91        assert_eq!(spec_bin(["status"], "qctl"), None);
92        assert_eq!(spec_bin(["status", "--", "--usage-spec"], "qctl"), None);
93        assert_eq!(
94            spec_bin(["--usage-spec=q", "--", "status"], "qctl").as_deref(),
95            Some("q")
96        );
97    }
98
99    #[test]
100    fn spec_names_the_mounted_bin() {
101        let text = spec(Toy::command(), "q");
102        assert!(text.contains("name") && text.contains("status"), "{text}");
103    }
104
105    #[test]
106    fn mount_line_is_the_mise_bootstrap() {
107        assert_eq!(
108            mount_line("q"),
109            r#"#USAGE mount "mise run --quiet q -- --usage-spec=q""#
110        );
111    }
112}