use std::collections::HashMap;
use std::ffi::OsStr;
use usage::miette::Result;
use usage_rs::{Cli as DeriveCli, Subcommands};
mod complete_delegate;
pub mod complete_word;
mod diff;
mod exec;
mod explain;
pub(crate) mod generate;
mod lint;
mod mcp;
mod shell;
mod sponsors;
#[derive(DeriveCli)]
#[usage(completion = true)]
#[usage(
bin = "usage",
version,
// The project's mark in block characters: the flag on its pole, the two dashes it carries,
// and the copies behind it. In a file rather than in the attribute because art is edited by
// looking at it, and a raw string indented to match this list would not be what prints.
logo = include_str!("logo.txt"),
logo_style = "magenta",
min_usage_version = "6.5",
repository = "https://github.com/jdx/usage",
// The command path is not the file path: command names are hyphenated where the files
// that implement them are snake_case, a command with subcommands lives in its directory's
// `mod.rs`, and the four shell commands are all served by a single `shell.rs`.
//
// Unindented, because a raw string keeps every leading space it is given and only the
// `{%-`/`-%}` markers take any back — so indenting to match the attribute would be
// trusting each line to be surrounded by them.
source_code_link_template = r#"{%- set path = path | replace(from='-', to='_') -%}
{%- if cmd.subcommands | length > 0 -%}
{%- set path = path ~ "/mod.rs" -%}
{%- elif path in ["bash", "fish", "powershell", "zsh"] -%}
{%- set path = "shell.rs" -%}
{%- else -%}
{%- set path = path ~ ".rs" -%}
{%- endif -%}
https://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}"#,
usage = "Usage: usage <COMMAND>\n usage --completions <COMPLETIONS>\n usage --usage-spec",
// Every flag `usage` accepts is one it declares, so an unrecognised one is a mistake and
// saying so beats offering it to a positional — `usage lint --nope f.kdl` would otherwise
// make `--nope` the file and call the real file unexpected.
//
// Declared once, on the root: every subcommand inherits it. The five that hand a command
// line to somebody else's script say `value` for themselves, which is the whole reason
// both halves are needed.
unknown_flags = "error"
)]
pub struct Cli {
#[usage(subcommand)]
command: Command,
#[usage(long)]
completions: Option<String>,
#[usage(long)]
usage_spec: bool,
}
pub(crate) fn version() -> String {
let spec = Cli::spec();
format!(
"{} {}",
spec.bin.unwrap_or(spec.name),
spec.version.unwrap_or(env!("CARGO_PKG_VERSION"))
)
}
#[derive(Subcommands)]
#[usage(run)]
enum Command {
Bash(shell::Bash),
CompleteWord(complete_word::CompleteWord),
Diff(diff::Diff),
Exec(exec::Exec),
Explain(explain::Explain),
Fish(shell::Fish),
Generate(generate::Generate),
Lint(lint::Lint),
Mcp(mcp::Mcp),
#[usage(name = "powershell")]
PowerShell(shell::PowerShell),
Sponsors(sponsors::Sponsors),
Zsh(shell::Zsh),
}
impl Cli {
pub fn run(argv: &[String]) -> Result<()> {
let words: Vec<&OsStr> = argv.iter().skip(1).map(OsStr::new).collect();
if let [command, help] = words.as_slice()
&& matches!(command.to_str(), Some("exec" | "x"))
&& matches!(help.to_str(), Some("-h" | "--help"))
{
let spec = Self::spec();
let exec = spec
.root
.subcommands
.iter()
.find(|subcommand| subcommand.cmd.name == "exec")
.expect("the CLI declares its exec command");
if let Some(page) = usage_rs::help::render_styled(
spec,
exec.cmd,
*help == OsStr::new("--help"),
usage_rs::help::Style::auto(),
) {
print!("{page}");
}
return Ok(());
}
shell::note_supplied_words(&words);
let cli = match Self::parse_from(&words) {
Ok(cli) => cli,
Err(usage_rs::Error::Help { cmd, long }) => {
if let Some(page) = usage_rs::help::render_styled(
Self::spec(),
cmd,
long,
usage_rs::help::Style::auto(),
) {
print!("{page}");
}
return Ok(());
}
Err(usage_rs::Error::HelpAll { cmd }) => {
if let Some(page) = usage_rs::help::render_all_styled(
Self::spec(),
cmd,
usage_rs::help::Style::auto(),
) {
print!("{page}");
}
return Ok(());
}
Err(usage_rs::Error::Version { .. }) => {
println!("{}", version());
return Ok(());
}
Err(err) => {
eprint!("{}", usage_rs::render_failure(Self::spec(), &words, &err));
std::process::exit(2);
}
};
if let Some(shell) = cli.completions.as_deref() {
return crate::usage_spec::complete(shell);
}
if cli.usage_spec {
return crate::usage_spec::generate();
}
usage_rs::Run::run(cli.command)
}
}
pub(crate) fn empty_mount_answers(cmd: &usage::SpecCommand) -> HashMap<String, String> {
let mut answers = HashMap::new();
collect_mount_answers(cmd, &mut answers);
answers
}
fn collect_mount_answers(cmd: &usage::SpecCommand, answers: &mut HashMap<String, String>) {
for mount in &cmd.mounts {
answers.insert(
mount.run.clone(),
"name \"mounted\"\nbin \"mounted\"\n".to_string(),
);
}
for sub in cmd.subcommands.values() {
collect_mount_answers(sub, answers);
}
}
#[derive(Debug, Clone, Copy, Default, usage_rs::ValueEnum)]
pub(crate) enum OutputFormat {
#[default]
Text,
Json,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
use usage_rs::spec::ValueEnum;
Self::from_choice(value).ok_or_else(|| {
format!(
"`{value}` is not one of: {}",
Self::ACCEPTED_CHOICES.join(", ")
)
})
}
}