1#![cfg_attr(
13 feature = "app",
14 doc = "",
15 doc = "[`App::mounted_as`]: crate::App::mounted_as",
16 doc = "[`App`]: crate::App"
17)]
18#![cfg_attr(
19 not(feature = "app"),
20 doc = "",
21 doc = "[`App::mounted_as`]: crate#cargo-features",
22 doc = "[`App`]: crate#cargo-features"
23)]
24
25mod kdl;
26mod nodes;
27mod plain;
28
29use std::io::Write as _;
30use std::process::ExitCode;
31
32use clap::Command;
33
34use crate::formatdoc;
35
36#[must_use]
47pub fn spec(mut command: Command, bin: &str) -> String {
48 command.set_bin_name(bin);
49 nodes::document(command, bin).to_string()
50}
51
52#[must_use]
58pub fn mount_line(task: &str) -> String {
59 formatdoc! {r#"#USAGE mount "mise run --quiet {task} -- --usage-spec={task}""#}
60}
61
62#[must_use]
75pub fn mount_directives(task: &str) -> String {
76 formatdoc! {"
77 #MISE raw=true
78 #MISE raw_args=true
79 {mount}",
80 mount = mount_line(task),
81 }
82}
83
84#[must_use]
91pub fn take<C: clap::CommandFactory>(default_bin: &str) -> Option<ExitCode> {
92 let bin = spec_bin(std::env::args().skip(1), default_bin)?;
93 let written = write!(std::io::stdout().lock(), "{}", spec(C::command(), &bin));
94 Some(if written.is_ok() {
95 ExitCode::SUCCESS
96 } else {
97 ExitCode::FAILURE
98 })
99}
100
101#[must_use]
108pub fn spec_bin(
109 args: impl IntoIterator<Item = impl AsRef<str>>,
110 default_bin: &str,
111) -> Option<String> {
112 for arg in args {
113 let arg = arg.as_ref();
114 if arg == "--" {
115 break;
116 }
117 if arg == "--usage-spec" {
118 return Some(default_bin.to_owned());
119 }
120 if let Some(bin) = arg.strip_prefix("--usage-spec=") {
121 return Some(if bin.is_empty() {
122 default_bin.to_owned()
123 } else {
124 bin.to_owned()
125 });
126 }
127 }
128 None
129}
130
131#[cfg(test)]
132mod tests {
133 use clap::{CommandFactory, Parser};
134
135 use super::{mount_directives, mount_line, spec, spec_bin};
136
137 #[derive(Parser)]
138 #[command(name = "toy")]
139 struct Toy {
140 #[command(subcommand)]
141 command: ToyCommand,
142 }
143
144 #[derive(clap::Subcommand)]
145 enum ToyCommand {
146 Status,
147 Check,
148 }
149
150 #[test]
151 fn spec_bin_reads_equals_and_bare() {
152 assert_eq!(spec_bin(["--usage-spec"], "qctl").as_deref(), Some("qctl"));
153 assert_eq!(spec_bin(["--usage-spec=q"], "qctl").as_deref(), Some("q"));
154 assert_eq!(spec_bin(["status"], "qctl"), None);
155 assert_eq!(spec_bin(["status", "--", "--usage-spec"], "qctl"), None);
156 assert_eq!(
157 spec_bin(["--usage-spec=q", "--", "status"], "qctl").as_deref(),
158 Some("q")
159 );
160 }
161
162 #[test]
163 fn spec_names_the_mounted_bin() {
164 let text = spec(Toy::command(), "q");
165 assert!(text.contains("name") && text.contains("status"), "{text}");
166 }
167
168 #[test]
169 fn mount_line_is_the_mise_bootstrap() {
170 assert_eq!(
171 mount_line("q"),
172 r#"#USAGE mount "mise run --quiet q -- --usage-spec=q""#
173 );
174 }
175
176 #[test]
177 fn mount_directives_end_with_the_mount_line() {
178 let directives = mount_directives("q");
179 assert_eq!(
180 directives.lines().collect::<Vec<_>>(),
181 [
182 "#MISE raw=true",
183 "#MISE raw_args=true",
184 mount_line("q").as_str()
185 ]
186 );
187 }
188}