//! Shell completion scripts, generated on demand from the clap command tree. //! //! The surface is around twenty-five subcommands across `auth`, `pr` and //! `repo`, and several of them take record keys or `at://` URIs that nobody //! can type from memory. Completion is the difference between `atgc pr //! res` and looking the verb up. //! //! Nothing here is generated at build time and nothing is checked in. The //! script is a projection of `Cli`, so a checked-in copy is wrong from the //! moment a flag moves and there is no CI run to notice: the workflow in //! `.tangled/workflows/ci.yml` has never executed. `atgc completion bash` //! reads the command tree out of the binary you are actually running, so it //! cannot disagree with it. The cost is that a user has to run one command //! after install, and re-run it after an upgrade if they want the new flags; //! that is the trade, and README.md says so plainly. //! //! It is also deliberately *static* completion: a script the shell runs by //! itself, rather than `clap_complete`'s `unstable-dynamic` mode, where the //! shell calls back into `atgc` on every TAB. See the module's tests and the //! README for the argument; the short version is that dynamic mode routes //! *all* completion through a process spawn, and the values worth completing //! here (pull request keys, repo names) live behind Bobbin, which has been //! observed more than a day behind its own firehose. use anyhow::Result; use clap::CommandFactory; use clap_complete::Shell; #[derive(Debug)] pub(crate) struct Args { pub shell: Shell, } /// Write a completion script for `shell` to stdout. /// /// Stdout and not a file: the caller decides where it goes, which is what /// lets one documented line cover both "write it into the completions /// directory" and "eval it here". Writing files on the user's behalf would /// mean guessing at a completions directory per shell, and getting that /// wrong silently is worse than not doing it. pub(crate) fn completion(args: Args) -> Result<()> { let mut cmd = crate::Cli::command(); // `get_name` rather than a literal "atgc": if the binary is ever renamed // or aliased through `[[bin]]`, the script must name whatever clap thinks // the program is called, or it registers against a command nobody runs. let name = cmd.get_name().to_string(); clap_complete::generate(args.shell, &mut cmd, name, &mut std::io::stdout()); Ok(()) } #[cfg(test)] mod tests { use super::*; use clap_complete::Shell; /// Generation must not panic, and must name the top-level verbs. /// /// Most of what this asserts is clap's own guarantee and would be a test /// of the library rather than of this crate. One part is not: the /// generators are free to panic on a command tree they cannot render, and /// several of them interpolate help text straight into the script they /// emit. This tree's help is full of apostrophes ("the checkout's own"), /// em dashes, backticks and multi-line `verbatim_doc_comment` blocks /// containing `at://` URIs and a bare `*`, all of which are metacharacters /// in at least one of the five output languages. Whether the resulting /// script is syntactically valid is not checkable here: that needs the /// shell itself, but a generator that /// gives up outright is, and that is what this pins. #[test] fn generates_for_every_shell() { for shell in [ Shell::Bash, Shell::Zsh, Shell::Fish, Shell::PowerShell, Shell::Elvish, ] { let mut cmd = crate::Cli::command(); let mut out = Vec::new(); clap_complete::generate(shell, &mut cmd, "atgc", &mut out); let script = String::from_utf8(out).expect("completion script is not UTF-8"); assert!(!script.is_empty(), "{shell} produced an empty script"); for verb in ["auth", "browse", "completion", "logs", "pr", "repo"] { assert!( script.contains(verb), "{shell} script does not mention `{verb}`" ); } } } /// Every top-level command the help lists is offered by the scripts. /// /// This test used to assert the opposite of its name: a top-level /// `login` was `hide = true`, clap_complete's ahead-of-time generators /// consult `is_hide_set` for possible *values* and never for /// subcommands, and so the one command absent from `--help` was present /// in every script. That mismatch is gone because the alias is gone — /// nothing in this tree is hidden now — so what is worth pinning is the /// agreement itself: what the shell offers is what `--help` lists. #[test] fn the_scripts_offer_what_the_help_lists() { let mut cmd = crate::Cli::command(); let mut out = Vec::new(); clap_complete::generate(Shell::Bash, &mut cmd, "atgc", &mut out); let script = String::from_utf8(out).unwrap(); // The top-level word list, which is what `atgc ` offers. let top = script .lines() .find(|l| l.contains("opts=") && l.contains("about")) .expect("bash script has no top-level subcommand list"); // The words of `opts="…"`, which is a shell string rather than // anything structured, so the quotes and the `-h`/`--help` flags in // it are split off rather than parsed. let offered: Vec<&str> = top .split(['"', ' ', '=']) .filter(|w| !w.is_empty()) .collect(); for name in crate::Cli::command() .get_subcommands() .filter(|c| !c.is_hide_set()) .map(clap::Command::get_name) { assert!( offered.contains(&name), "`{name}` is in --help but not in the bash script's top-level list:\n{top}" ); } } }