//! `didbot` — the one command a person types, and nothing more than a //! router. //! //! `didbot …` finds the binary that answers `` and replaces //! itself with it, handing over the rest of the command line. The //! first-party verbs are in [`verbs::FIRST_PARTY`], each naming its binary, //! so a verb whose binary is not installed is answered with what to install. //! Any other word is `didbot-` on `PATH`, which is how someone adds a //! verb without touching this repository. //! //! The dispatcher passes argv and nothing else. It adds nothing to the //! environment and removes every variable in [`didbot_cli::env::CREDENTIALS`] //! before handing off: a verb reached this way authenticates for itself, and //! a program reachable by a typo on `didbot-` inherits no credential from the //! shell that ran it. //! //! `didbot --list` scans `PATH` for `didbot-*`, asks each for `--version` and //! prints what it found. Every binary is versioned on its own, so a mix of //! versions shows up here and fails nowhere. #![forbid(unsafe_code)] mod path; mod run; mod verbs; use std::ffi::OsString; use std::process::ExitCode; use clap::{CommandFactory, Parser, Subcommand}; use didbot_cli::{finish, FirstPartyVerb, Refusal}; use crate::verbs::FIRST_PARTY; /// The spelling every refusal from this binary is prefixed with. const PROGRAM: &str = "didbot"; #[derive(Parser)] #[command( name = PROGRAM, version, about = "The didbot commands, one entry point", long_about = "Runs `didbot-` from PATH with the rest of the command line, and \ nothing else: no credential from this shell reaches the verb.", after_help = verbs::help_table(), allow_external_subcommands = true, disable_help_subcommand = true, arg_required_else_help = true, args_conflicts_with_subcommands = true )] struct Cli { /// Every didbot-* on PATH, with its version #[arg(long)] list: bool, #[command(subcommand)] verb: Option, } #[derive(Subcommand)] enum Verb { #[command(external_subcommand)] External(Vec), } fn main() -> ExitCode { let cli: Cli = didbot_cli::parse(); if cli.list { return finish(PROGRAM, list()); } let Some(Verb::External(words)) = cli.verb else { // Unreachable through clap: `arg_required_else_help` answers the // empty command line, and `--list` returned above. return finish( PROGRAM, Err(Refusal::usage("a verb is required; see --help")), ); }; let (verb, rest) = words .split_first() .expect("an external subcommand has a name"); let verb = verb.to_string_lossy(); // `didbot help ` is `didbot --help`, resolved the same way. if verb == "help" { return match rest.split_first() { None => { let _ = Cli::command().print_help(); ExitCode::SUCCESS } Some((asked, _)) => { let asked = asked.to_string_lossy(); finish(PROGRAM, dispatch(&asked, &[OsString::from("--help")])) } }; } finish(PROGRAM, dispatch(&verb, rest)) } /// Finds the binary for `verb` and runs it with `argv`. /// /// Only returns when the binary is not there, or could not be started: on /// success the verb's process replaces this one. fn dispatch(verb: &str, argv: &[OsString]) -> Result<(), Refusal> { match FirstPartyVerb::lookup(FIRST_PARTY, verb) { Some(row) => { let binary = path::find(row.binary).ok_or_else(|| { Refusal::failed(format!( "`{verb}` runs {}, which is not on PATH; install {}", row.binary, row.package )) })?; let leading: Vec = row.argv.iter().map(OsString::from).collect(); run::exec(&binary, leading.iter().chain(argv)) } None => { let name = format!("didbot-{verb}"); let binary = path::find(&name).ok_or_else(|| { Refusal::usage(format!( "unknown command `{verb}`; `didbot --list` shows what is installed" )) })?; run::exec(&binary, argv) } } } /// `--list`: this binary, then every `didbot-*` on `PATH` with what it says /// to `--version`, then the first-party binaries that are not installed. fn list() -> Result<(), Refusal> { let own = std::env::current_exe() .map_err(|error| Refusal::failed(format!("cannot tell where this binary is: {error}")))?; let mut rows = vec![( PROGRAM.to_owned(), env!("CARGO_PKG_VERSION").to_owned(), own.display().to_string(), )]; for (name, binary) in path::installed() { let version = run::version_of(&binary) .and_then(|line| line.split_whitespace().nth(1).map(str::to_owned)) .unwrap_or_else(|| "?".to_owned()); rows.push((name, version, binary.display().to_string())); } let name_width = rows.iter().map(|row| row.0.len()).max().unwrap_or(0); let version_width = rows.iter().map(|row| row.1.len()).max().unwrap_or(0); for (name, version, location) in &rows { println!("{name: = FIRST_PARTY .iter() .map(|row| row.binary) .filter(|binary| !rows.iter().any(|row| row.0 == *binary)) .collect(); missing.dedup(); for binary in missing { let verbs: Vec<&str> = FIRST_PARTY .iter() .filter(|row| row.binary == binary) .map(|row| row.verb) .collect(); println!("{binary}: not installed ({})", verbs.join(", ")); } Ok(()) }