use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use clap::{Args, CommandFactory};
use clap_complete::Shell;
use crate::cli::Cli;
const BIN_NAME: &str = "cabin";
const ALL_SHELLS: &[Shell] = &[
Shell::Bash,
Shell::Zsh,
Shell::Fish,
Shell::PowerShell,
Shell::Elvish,
];
#[derive(Debug, Args)]
pub(crate) struct CompgenArgs {
#[arg(value_enum, conflicts_with = "all", required_unless_present = "all")]
shell: Option<Shell>,
#[arg(long)]
all: bool,
#[arg(long, value_name = "PATH")]
output_dir: Option<std::path::PathBuf>,
}
pub(crate) fn run(args: &CompgenArgs) -> Result<()> {
if args.all && args.output_dir.is_none() {
bail!(
"`--all` requires `--output-dir`; multiple completion files cannot be written to stdout cleanly"
);
}
let mut cmd = Cli::command();
if args.all {
let dir = args.output_dir.as_ref().expect("checked above");
write_all(&mut cmd, dir)?;
return Ok(());
}
let shell = args.shell.expect("clap enforces required-unless-all");
match args.output_dir.as_ref() {
Some(dir) => write_one_to_dir(&mut cmd, shell, dir)?,
None => write_one_to_stdout(&mut cmd, shell),
}
Ok(())
}
fn write_all(cmd: &mut clap::Command, dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.with_context(|| format!("failed to create completion output dir {}", dir.display()))?;
for shell in ALL_SHELLS {
write_one_to_dir(cmd, *shell, dir)?;
}
Ok(())
}
fn write_one_to_dir(cmd: &mut clap::Command, shell: Shell, dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.with_context(|| format!("failed to create completion output dir {}", dir.display()))?;
let path = dir.join(filename_for(shell));
let mut file =
fs::File::create(&path).with_context(|| format!("failed to create {}", path.display()))?;
clap_complete::generate(shell, cmd, BIN_NAME, &mut file);
Ok(())
}
fn write_one_to_stdout(cmd: &mut clap::Command, shell: Shell) {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
clap_complete::generate(shell, cmd, BIN_NAME, &mut handle);
}
fn filename_for(shell: Shell) -> String {
match shell {
Shell::Bash => "cabin.bash".to_owned(),
Shell::Zsh => "_cabin".to_owned(),
Shell::Fish => "cabin.fish".to_owned(),
Shell::PowerShell => "cabin.ps1".to_owned(),
Shell::Elvish => "cabin.elv".to_owned(),
other => format!("cabin.{}", other.to_string().to_lowercase()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filenames_are_stable_across_shells() {
assert_eq!(filename_for(Shell::Bash), "cabin.bash");
assert_eq!(filename_for(Shell::Zsh), "_cabin");
assert_eq!(filename_for(Shell::Fish), "cabin.fish");
assert_eq!(filename_for(Shell::PowerShell), "cabin.ps1");
assert_eq!(filename_for(Shell::Elvish), "cabin.elv");
}
#[test]
fn all_shells_list_matches_supported_shells() {
for shell in ALL_SHELLS {
let name = filename_for(*shell);
assert!(!name.is_empty());
}
}
#[test]
fn cli_command_includes_compgen_and_mangen() {
let cmd = Cli::command();
let names: Vec<&str> = cmd.get_subcommands().map(clap::Command::get_name).collect();
for expected in ["compgen", "mangen"] {
assert!(
names.contains(&expected),
"missing subcommand {expected}; got {names:?}"
);
}
}
}