usage-cli 7.1.2

CLI for working with usage-based CLIs
Documentation
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;

/// Generate completions, docs, manpages, and SDKs from a usage spec, and run scripts that carry one
///
/// A usage spec describes a CLI's commands, flags, and arguments once, in KDL. Most commands
/// here read one: a `.usage.kdl` file given with `-f`, a script whose `#USAGE` comments declare
/// it, or `-` for stdin. A binary built with the Rust framework prints its own spec, so a
/// pipeline like `mycli __usage_spec__ | usage generate markdown -f -` needs no file at all.
///
/// Guides and a reference for every command: https://usage.jdx.dev/cli/
// `usage` parses its own command line with the parser it ships: the tables below are the same
// ones an adopter's CLI compiles into, and `--usage-spec` prints the spec they emit rather
// than a transcription of a clap command. Said here rather than in the doc comment, which is
// the help page a user reads.
//
// 3.6 added `effect=` and 4.0 added it on flags and args; older `usage` CLIs reject the spec
// outright with "unsupported cmd prop effect", so this moves in lockstep with the fields the
// spec actually carries.
//
// Owed a bump: the four shell commands flatten `Shell`, which now emits a `flagset`, and no
// 4.0 can read that node. The floor is whichever release carries flagsets, so the number waits
// for that release rather than being guessed at here — and this crate warning about its own
// spec until then is worse than the stale claim.
#[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,

    /// Print a completion script for the `usage` CLI itself, for the named shell
    // `--completions <shell>` is normally answered in `crate::run` before a parse happens,
    // because a shell init script asks for it on every new shell and it does not need a
    // subcommand to answer. It remains a real parsed field so direct callers of `Cli::run`
    // and the help, spec, and completions all read the same declaration.
    //
    // A flag, which is how it is typed. The clap declaration made it a *positional* — so the
    // spec, the docs and the generated completions all described a `[COMPLETIONS]` argument
    // that nothing accepts, while the flag that does work went undocumented. Carried over
    // faithfully at first, wrong included; the point of emitting the spec from the
    // declaration is that the two cannot disagree, so the declaration is what changes.
    #[usage(long)]
    completions: Option<String>,

    /// Print the usage spec for the `usage` CLI itself
    #[usage(long)]
    usage_spec: bool,
}

/// What `--version` and `-v` answer with.
///
/// The binary's name, not the crate's. They differ here — `usage-cli` ships `usage` — and
/// everything else this CLI says about itself now comes from the spec, where the name is
/// `usage`. Read from the spec rather than written out again, so a rename cannot leave the
/// version line saying something the help page above it contradicts.
pub(crate) fn version() -> String {
    let spec = Cli::spec();
    format!(
        "{} {}",
        spec.bin.unwrap_or(spec.name),
        spec.version.unwrap_or(env!("CARGO_PKG_VERSION"))
    )
}

/// What `usage` can be asked to do.
///
/// Each command's description is its struct's doc comment, in the file that owns it — and so
/// is the code that carries it out: `run` generates the match that hands the selected command
/// to its `usage_rs::Run` implementation, so this list is the only place a command is named.
#[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<()> {
        // `parse_from` takes the command line without the program name, and hands back what
        // went wrong instead of ending the process — which is what lets the error come out
        // through the same path as every other failure here.
        let words: Vec<&OsStr> = argv.iter().skip(1).map(OsStr::new).collect();
        // `exec` owns `-h` and `--help`: once COMMAND and BIN are present they describe the
        // wrapped script, not this command. With no script to describe, answer the ordinary
        // command-help request before its required positionals are checked. `usage help exec`
        // reaches the same page through the parser's synthesized help command.
        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,
            // Not failures: someone asked a question, and the answer goes to stdout.
            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));
                // clap's status for a command line it could not parse, which is what the
                // scripts that call this have been checking for.
                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();
        }
        // The match that used to be here, one arm per command, is generated from the enum
        // above — so a command added there cannot be left unrouted, and no arm can route to
        // the wrong handler.
        usage_rs::Run::run(cli.command)
    }
}

/// A spec that declares nothing, as the answer to every mount in the tree.
///
/// Keyed by the exact `run` string, which is how injected answers are looked up. It is a
/// whole spec rather than an empty string because that is what a mount's stdout is.
///
/// Shared by `lint` and `explain`, which want it for the same reason: usage-lib resolves a
/// command's mounts on the way *into* it, so a spec that mounts anything cannot be parsed
/// without either spawning the mounted program or being handed its answer — and a command
/// that reads a file and prints a report should not spawn whatever that file names.
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);
    }
}

/// How a command that can print either prose or JSON was asked to print.
///
/// Shared rather than declared twice: `lint` and `explain` both offer it, and a third
/// copy of the same four-line `FromStr` is how the two spellings drift apart.
#[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> {
        // Delegated rather than matched again: the derive already lists the words, and a
        // second list beside it is one more thing that can fall out of step with the type.
        use usage_rs::spec::ValueEnum;
        Self::from_choice(value).ok_or_else(|| {
            format!(
                "`{value}` is not one of: {}",
                Self::ACCEPTED_CHOICES.join(", ")
            )
        })
    }
}