diff --git a/Cargo.toml b/Cargo.toml index aaafd21..71d867f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ build = "build.rs" [dependencies] clap = { version = "4.6.6", features = ["derive"] } -# clap_complete = "4.5.35" +clap_complete = "4.6.9" anyhow = "1.0.104" dotenvy = "0.15.7" diff --git a/build.rs b/build.rs index 3266995..0d99eb1 100644 --- a/build.rs +++ b/build.rs @@ -1,22 +1,55 @@ use clap::{CommandFactory, ValueEnum}; use clap_complete::{Shell, generate_to}; -use std::{env, io::Error}; +use std::{env, ffi::OsString, fs, io::Error, path::PathBuf}; include!("src/cli/mod.rs"); -fn main() -> Result<(), Error> { - let outdir = env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| env::var_os("OUT_DIR")); +fn shell_name(shell: Shell) -> String { + shell + .to_possible_value() + .map(|v| v.get_name().to_owned()) + .unwrap_or_default() +} - let Some(outdir) = outdir else { - return Ok(()); - }; +fn non_empty(name: &str) -> Option { + env::var_os(name).filter(|v| !v.is_empty()) +} + +fn install_dir(shell: Shell) -> Option<(PathBuf, bool)> { + let per_shell = format!("SHELL_COMPLETIONS_DIR_{}", shell_name(shell).to_uppercase()); + + if let Some(dir) = non_empty(&per_shell).or_else(|| non_empty("SHELL_COMPLETIONS_DIR")) { + return Some((PathBuf::from(dir), true)); + } + non_empty("OUT_DIR").map(|dir| (PathBuf::from(dir), false)) +} + +fn main() -> Result<(), Error> { + println!("cargo::rerun-if-env-changed=SHELL_COMPLETIONS_DIR"); + for shell in Shell::value_variants() { + println!( + "cargo::rerun-if-env-changed=SHELL_COMPLETIONS_DIR_{}", + shell_name(*shell).to_uppercase() + ); + } let mut cmd = ahab::Ahab::command(); for shell in Shell::value_variants() { - let path = generate_to(*shell, &mut cmd, env!("CARGO_PKG_NAME"), &outdir)?; + let Some((dir, requested)) = install_dir(*shell) else { + continue; + }; - println!("cargo:warning=completion file is generated: {path:?}"); + fs::create_dir_all(&dir)?; + let path = generate_to(*shell, &mut cmd, env!("CARGO_PKG_NAME"), &dir)?; + + if requested { + println!( + "cargo::warning=installed {} completion: {}", + shell_name(*shell), + path.display() + ); + } } Ok(()) diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index d5d0388..f0f0784 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -1,5 +1,6 @@ use super::{Django, DockerCompose, Link, Postgres}; use clap::{Parser, Subcommand}; +use clap_complete::Shell; /// A program for interacting with various dockerized applications. #[derive(Parser, Debug)] @@ -34,4 +35,10 @@ pub enum Commands { #[command(subcommand)] command: Link, }, + + /// Print a shell completion script on stdout + Completions { + /// Shell to generate the script for + shell: Shell, + }, } diff --git a/src/main.rs b/src/main.rs index 51efbb7..c218255 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,5 +50,6 @@ fn main() -> Result<()> { null, } => scripts::link::check(&paths, porcelain, null), }, + cli::Commands::Completions { shell } => scripts::completions::completions(shell), } } diff --git a/src/scripts/completions.rs b/src/scripts/completions.rs new file mode 100644 index 0000000..949e0ef --- /dev/null +++ b/src/scripts/completions.rs @@ -0,0 +1,46 @@ +use std::io::{self, Write}; + +use anyhow::{Context, Result}; +use clap::CommandFactory; +use clap_complete::{Shell, generate}; + +use crate::cli::Ahab; + +pub fn completions(shell: Shell) -> Result<()> { + let script = script(shell); + + let mut stdout = io::stdout().lock(); + match stdout.write_all(&script).and_then(|()| stdout.flush()) { + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()), + result => result.context("writing the completion script"), + } +} + +fn script(shell: Shell) -> Vec { + let mut cmd = Ahab::command(); + let name = cmd.get_name().to_string(); + + let mut script = Vec::new(); + generate(shell, &mut cmd, name, &mut script); + script +} + +#[cfg(test)] +mod tests { + use super::{Shell, script}; + use clap::ValueEnum; + + #[test] + fn every_shell_gets_a_script_covering_the_subcommands() { + for shell in Shell::value_variants() { + let out = String::from_utf8(script(*shell)).expect("script is utf8"); + + for subcommand in ["compose", "django", "postgres", "link", "completions"] { + assert!( + out.contains(subcommand), + "{shell:?} script never mentions {subcommand}" + ); + } + } + } +} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index e977d24..4fbf51b 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -1,3 +1,4 @@ +pub mod completions; pub mod django; pub mod docker; pub mod docker_compose;