merge: completions improvements

This commit is contained in:
2026-09-04 16:03:46 +02:00
6 changed files with 97 additions and 9 deletions

View File

@@ -14,7 +14,7 @@ build = "build.rs"
[dependencies] [dependencies]
clap = { version = "4.6.6", features = ["derive"] } clap = { version = "4.6.6", features = ["derive"] }
# clap_complete = "4.5.35" clap_complete = "4.6.9"
anyhow = "1.0.104" anyhow = "1.0.104"
dotenvy = "0.15.7" dotenvy = "0.15.7"

View File

@@ -1,22 +1,55 @@
use clap::{CommandFactory, ValueEnum}; use clap::{CommandFactory, ValueEnum};
use clap_complete::{Shell, generate_to}; 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"); include!("src/cli/mod.rs");
fn main() -> Result<(), Error> { fn shell_name(shell: Shell) -> String {
let outdir = env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| env::var_os("OUT_DIR")); shell
.to_possible_value()
.map(|v| v.get_name().to_owned())
.unwrap_or_default()
}
let Some(outdir) = outdir else { fn non_empty(name: &str) -> Option<OsString> {
return Ok(()); 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(); let mut cmd = ahab::Ahab::command();
for shell in Shell::value_variants() { 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(()) Ok(())

View File

@@ -1,5 +1,6 @@
use super::{Django, DockerCompose, Link, Postgres}; use super::{Django, DockerCompose, Link, Postgres};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use clap_complete::Shell;
/// A program for interacting with various dockerized applications. /// A program for interacting with various dockerized applications.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -34,4 +35,10 @@ pub enum Commands {
#[command(subcommand)] #[command(subcommand)]
command: Link, command: Link,
}, },
/// Print a shell completion script on stdout
Completions {
/// Shell to generate the script for
shell: Shell,
},
} }

View File

@@ -50,5 +50,6 @@ fn main() -> Result<()> {
null, null,
} => scripts::link::check(&paths, porcelain, null), } => scripts::link::check(&paths, porcelain, null),
}, },
cli::Commands::Completions { shell } => scripts::completions::completions(shell),
} }
} }

View File

@@ -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<u8> {
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}"
);
}
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod completions;
pub mod django; pub mod django;
pub mod docker; pub mod docker;
pub mod docker_compose; pub mod docker_compose;