usage_cli/cli/mod.rs
1use std::collections::HashMap;
2use std::ffi::OsStr;
3
4use usage::miette::Result;
5use usage_rs::{Cli as DeriveCli, Subcommands};
6
7mod complete_delegate;
8pub mod complete_word;
9mod diff;
10mod exec;
11mod explain;
12pub(crate) mod generate;
13mod lint;
14mod mcp;
15mod shell;
16mod sponsors;
17
18/// Generate completions, docs, manpages, and SDKs from a usage spec, and run scripts that carry one
19///
20/// A usage spec describes a CLI's commands, flags, and arguments once, in KDL. Most commands
21/// here read one: a `.usage.kdl` file given with `-f`, a script whose `#USAGE` comments declare
22/// it, or `-` for stdin. A binary built with the Rust framework prints its own spec, so a
23/// pipeline like `mycli __usage_spec__ | usage generate markdown -f -` needs no file at all.
24///
25/// Guides and a reference for every command: https://usage.jdx.dev/cli/
26// `usage` parses its own command line with the parser it ships: the tables below are the same
27// ones an adopter's CLI compiles into, and `--usage-spec` prints the spec they emit rather
28// than a transcription of a clap command. Said here rather than in the doc comment, which is
29// the help page a user reads.
30//
31// 3.6 added `effect=` and 4.0 added it on flags and args; older `usage` CLIs reject the spec
32// outright with "unsupported cmd prop effect", so this moves in lockstep with the fields the
33// spec actually carries.
34//
35// Owed a bump: the four shell commands flatten `Shell`, which now emits a `flagset`, and no
36// 4.0 can read that node. The floor is whichever release carries flagsets, so the number waits
37// for that release rather than being guessed at here — and this crate warning about its own
38// spec until then is worse than the stale claim.
39#[derive(DeriveCli)]
40#[usage(completion = true)]
41#[usage(
42 bin = "usage",
43 version,
44 // The project's mark in block characters: the flag on its pole, the two dashes it carries,
45 // and the copies behind it. In a file rather than in the attribute because art is edited by
46 // looking at it, and a raw string indented to match this list would not be what prints.
47 logo = include_str!("logo.txt"),
48 logo_style = "magenta",
49 min_usage_version = "6.5",
50 repository = "https://github.com/jdx/usage",
51 // The command path is not the file path: command names are hyphenated where the files
52 // that implement them are snake_case, a command with subcommands lives in its directory's
53 // `mod.rs`, and the four shell commands are all served by a single `shell.rs`.
54 //
55 // Unindented, because a raw string keeps every leading space it is given and only the
56 // `{%-`/`-%}` markers take any back — so indenting to match the attribute would be
57 // trusting each line to be surrounded by them.
58 source_code_link_template = r#"{%- set path = path | replace(from='-', to='_') -%}
59{%- if cmd.subcommands | length > 0 -%}
60{%- set path = path ~ "/mod.rs" -%}
61{%- elif path in ["bash", "fish", "powershell", "zsh"] -%}
62{%- set path = "shell.rs" -%}
63{%- else -%}
64{%- set path = path ~ ".rs" -%}
65{%- endif -%}
66https://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}"#,
67 usage = "Usage: usage <COMMAND>\n usage --completions <COMPLETIONS>\n usage --usage-spec",
68 // Every flag `usage` accepts is one it declares, so an unrecognised one is a mistake and
69 // saying so beats offering it to a positional — `usage lint --nope f.kdl` would otherwise
70 // make `--nope` the file and call the real file unexpected.
71 //
72 // Declared once, on the root: every subcommand inherits it. The five that hand a command
73 // line to somebody else's script say `value` for themselves, which is the whole reason
74 // both halves are needed.
75 unknown_flags = "error"
76)]
77pub struct Cli {
78 #[usage(subcommand)]
79 command: Command,
80
81 /// Print a completion script for the `usage` CLI itself, for the named shell
82 // `--completions <shell>` is normally answered in `crate::run` before a parse happens,
83 // because a shell init script asks for it on every new shell and it does not need a
84 // subcommand to answer. It remains a real parsed field so direct callers of `Cli::run`
85 // and the help, spec, and completions all read the same declaration.
86 //
87 // A flag, which is how it is typed. The clap declaration made it a *positional* — so the
88 // spec, the docs and the generated completions all described a `[COMPLETIONS]` argument
89 // that nothing accepts, while the flag that does work went undocumented. Carried over
90 // faithfully at first, wrong included; the point of emitting the spec from the
91 // declaration is that the two cannot disagree, so the declaration is what changes.
92 #[usage(long)]
93 completions: Option<String>,
94
95 /// Print the usage spec for the `usage` CLI itself
96 #[usage(long)]
97 usage_spec: bool,
98}
99
100/// What `--version` and `-v` answer with.
101///
102/// The binary's name, not the crate's. They differ here — `usage-cli` ships `usage` — and
103/// everything else this CLI says about itself now comes from the spec, where the name is
104/// `usage`. Read from the spec rather than written out again, so a rename cannot leave the
105/// version line saying something the help page above it contradicts.
106pub(crate) fn version() -> String {
107 let spec = Cli::spec();
108 format!(
109 "{} {}",
110 spec.bin.unwrap_or(spec.name),
111 spec.version.unwrap_or(env!("CARGO_PKG_VERSION"))
112 )
113}
114
115/// What `usage` can be asked to do.
116///
117/// Each command's description is its struct's doc comment, in the file that owns it — and so
118/// is the code that carries it out: `run` generates the match that hands the selected command
119/// to its `usage_rs::Run` implementation, so this list is the only place a command is named.
120#[derive(Subcommands)]
121#[usage(run)]
122enum Command {
123 Bash(shell::Bash),
124 CompleteWord(complete_word::CompleteWord),
125 Diff(diff::Diff),
126 Exec(exec::Exec),
127 Explain(explain::Explain),
128 Fish(shell::Fish),
129 Generate(generate::Generate),
130 Lint(lint::Lint),
131 Mcp(mcp::Mcp),
132 #[usage(name = "powershell")]
133 PowerShell(shell::PowerShell),
134 Sponsors(sponsors::Sponsors),
135 Zsh(shell::Zsh),
136}
137
138impl Cli {
139 pub fn run(argv: &[String]) -> Result<()> {
140 // `parse_from` takes the command line without the program name, and hands back what
141 // went wrong instead of ending the process — which is what lets the error come out
142 // through the same path as every other failure here.
143 let words: Vec<&OsStr> = argv.iter().skip(1).map(OsStr::new).collect();
144 // `exec` owns `-h` and `--help`: once COMMAND and BIN are present they describe the
145 // wrapped script, not this command. With no script to describe, answer the ordinary
146 // command-help request before its required positionals are checked. `usage help exec`
147 // reaches the same page through the parser's synthesized help command.
148 if let [command, help] = words.as_slice()
149 && matches!(command.to_str(), Some("exec" | "x"))
150 && matches!(help.to_str(), Some("-h" | "--help"))
151 {
152 let spec = Self::spec();
153 let exec = spec
154 .root
155 .subcommands
156 .iter()
157 .find(|subcommand| subcommand.cmd.name == "exec")
158 .expect("the CLI declares its exec command");
159 if let Some(page) = usage_rs::help::render_styled(
160 spec,
161 exec.cmd,
162 *help == OsStr::new("--help"),
163 usage_rs::help::Style::auto(),
164 ) {
165 print!("{page}");
166 }
167 return Ok(());
168 }
169 shell::note_supplied_words(&words);
170 let cli = match Self::parse_from(&words) {
171 Ok(cli) => cli,
172 // Not failures: someone asked a question, and the answer goes to stdout.
173 Err(usage_rs::Error::Help { cmd, long }) => {
174 if let Some(page) = usage_rs::help::render_styled(
175 Self::spec(),
176 cmd,
177 long,
178 usage_rs::help::Style::auto(),
179 ) {
180 print!("{page}");
181 }
182 return Ok(());
183 }
184 Err(usage_rs::Error::HelpAll { cmd }) => {
185 if let Some(page) = usage_rs::help::render_all_styled(
186 Self::spec(),
187 cmd,
188 usage_rs::help::Style::auto(),
189 ) {
190 print!("{page}");
191 }
192 return Ok(());
193 }
194 Err(usage_rs::Error::Version { .. }) => {
195 println!("{}", version());
196 return Ok(());
197 }
198 Err(err) => {
199 eprint!("{}", usage_rs::render_failure(Self::spec(), &words, &err));
200 // clap's status for a command line it could not parse, which is what the
201 // scripts that call this have been checking for.
202 std::process::exit(2);
203 }
204 };
205 if let Some(shell) = cli.completions.as_deref() {
206 return crate::usage_spec::complete(shell);
207 }
208 if cli.usage_spec {
209 return crate::usage_spec::generate();
210 }
211 // The match that used to be here, one arm per command, is generated from the enum
212 // above — so a command added there cannot be left unrouted, and no arm can route to
213 // the wrong handler.
214 usage_rs::Run::run(cli.command)
215 }
216}
217
218/// A spec that declares nothing, as the answer to every mount in the tree.
219///
220/// Keyed by the exact `run` string, which is how injected answers are looked up. It is a
221/// whole spec rather than an empty string because that is what a mount's stdout is.
222///
223/// Shared by `lint` and `explain`, which want it for the same reason: usage-lib resolves a
224/// command's mounts on the way *into* it, so a spec that mounts anything cannot be parsed
225/// without either spawning the mounted program or being handed its answer — and a command
226/// that reads a file and prints a report should not spawn whatever that file names.
227pub(crate) fn empty_mount_answers(cmd: &usage::SpecCommand) -> HashMap<String, String> {
228 let mut answers = HashMap::new();
229 collect_mount_answers(cmd, &mut answers);
230 answers
231}
232
233fn collect_mount_answers(cmd: &usage::SpecCommand, answers: &mut HashMap<String, String>) {
234 for mount in &cmd.mounts {
235 answers.insert(
236 mount.run.clone(),
237 "name \"mounted\"\nbin \"mounted\"\n".to_string(),
238 );
239 }
240 for sub in cmd.subcommands.values() {
241 collect_mount_answers(sub, answers);
242 }
243}
244
245/// How a command that can print either prose or JSON was asked to print.
246///
247/// Shared rather than declared twice: `lint` and `explain` both offer it, and a third
248/// copy of the same four-line `FromStr` is how the two spellings drift apart.
249#[derive(Debug, Clone, Copy, Default, usage_rs::ValueEnum)]
250pub(crate) enum OutputFormat {
251 #[default]
252 Text,
253 Json,
254}
255
256impl std::str::FromStr for OutputFormat {
257 type Err = String;
258
259 fn from_str(value: &str) -> Result<Self, Self::Err> {
260 // Delegated rather than matched again: the derive already lists the words, and a
261 // second list beside it is one more thing that can fall out of step with the type.
262 use usage_rs::spec::ValueEnum;
263 Self::from_choice(value).ok_or_else(|| {
264 format!(
265 "`{value}` is not one of: {}",
266 Self::ACCEPTED_CHOICES.join(", ")
267 )
268 })
269 }
270}