use std::io::{self, Write};
use clap::CommandFactory;
use clap_complete::Shell;
use crate::cli::{Cli, CompletionArgs};
use crate::error::Result;
pub fn run(args: &CompletionArgs) -> Result<()> {
io::stdout().write_all(generate_script(args.shell).as_bytes())?;
Ok(())
}
fn generate_script(shell: Shell) -> String {
let mut cmd = Cli::command();
let bin_name = cmd.get_name().to_string();
let hidden: Vec<String> = cmd
.get_subcommands()
.filter(|sc| sc.is_hide_set())
.map(|sc| sc.get_name().to_string())
.collect();
let mut buf = Vec::new();
clap_complete::generate(shell, &mut cmd, bin_name, &mut buf);
let script = String::from_utf8(buf).expect("completion scripts are UTF-8");
strip_hidden_menu_entries(&script, &hidden, shell)
}
fn strip_hidden_menu_entries(script: &str, hidden: &[String], shell: Shell) -> String {
if hidden.is_empty() {
return script.to_string();
}
let mut out: Vec<String> = Vec::with_capacity(script.lines().count());
'lines: for line in script.lines() {
for name in hidden {
let is_menu_entry = match shell {
Shell::Zsh => line.trim_start().starts_with(&format!("'{name}:")),
Shell::Fish => line.contains(&format!("-a \"{name}\"")),
Shell::PowerShell => line.contains(&format!("[CompletionResult]::new('{name}'")),
Shell::Elvish => line.trim_start().starts_with(&format!("cand {name} ")),
_ => false,
};
if is_menu_entry {
continue 'lines;
}
}
let mut kept = line.to_string();
if shell == Shell::Bash && kept.trim_start().starts_with("opts=") {
for name in hidden {
kept = kept
.replace(&format!(" {name} "), " ")
.replace(&format!("\"{name} "), "\"")
.replace(&format!(" {name}\""), "\"");
}
}
out.push(kept);
}
let mut result = out.join("\n");
result.push('\n');
result
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_SHELLS: [Shell; 5] = [
Shell::Bash,
Shell::Zsh,
Shell::Fish,
Shell::PowerShell,
Shell::Elvish,
];
#[test]
fn generates_for_each_shell() {
for shell in ALL_SHELLS {
run(&CompletionArgs { shell }).expect("completion generation should succeed");
}
}
#[test]
fn scripts_offer_current_commands() {
for shell in ALL_SHELLS {
let script = generate_script(shell);
for name in ["encrypt", "decrypt", "types"] {
assert!(
script.contains(name),
"{shell:?} script should offer `{name}`"
);
}
assert!(
!script.contains("list-supported-types"),
"{shell:?} script should not offer the hidden alias"
);
}
}
#[test]
fn hidden_secrets_is_not_advertised() {
for (shell, menu_marker) in [
(Shell::Zsh, "'secrets:"),
(Shell::Fish, "-a \"secrets\""),
(Shell::PowerShell, "[CompletionResult]::new('secrets'"),
(Shell::Elvish, "cand secrets "),
] {
let script = generate_script(shell);
assert!(
!script.contains(menu_marker),
"{shell:?} script should not advertise `secrets`"
);
}
let bash = generate_script(Shell::Bash);
for line in bash.lines().filter(|l| l.trim_start().starts_with("opts=")) {
assert!(
!line.split(['"', ' ']).any(|word| word == "secrets"),
"bash opts should not list `secrets`: {line}"
);
}
}
#[test]
fn hidden_secrets_arm_is_kept_for_back_compat() {
let zsh = generate_script(Shell::Zsh);
assert!(zsh.contains("(secrets)"));
assert!(zsh.contains("_sopsy__secrets_commands") || zsh.contains("secrets_commands"));
}
}