use std::io::{self, Write};
use clap::CommandFactory;
use clap_complete::{Shell, generate};
use crate::args::Cli;
use crate::error::CliError;
const COMPLETION_BIN_NAME: &str = "seshat";
pub fn run_completions(shell: Option<Shell>) -> Result<(), CliError> {
let shell = match shell {
Some(s) => s,
None => detect_shell()?,
};
let mut cmd = Cli::command();
let stdout = io::stdout();
let mut handle = stdout.lock();
generate(shell, &mut cmd, COMPLETION_BIN_NAME, &mut handle);
match handle.flush() {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
Err(e) => Err(CliError::Io(e)),
}
}
fn detect_shell() -> Result<Shell, CliError> {
let raw_set = std::env::var("SHELL").ok();
let raw = raw_set
.as_deref()
.map(|s| s.trim().trim_end_matches('\r'))
.filter(|s| !s.is_empty());
if let Some(raw) = raw {
if let Some(name) = shell_basename(raw) {
if let Some(shell) = map_shell_name(name) {
return Ok(shell);
}
return Err(CliError::InvalidArgument(format!(
"could not auto-detect shell from $SHELL={raw:?} (basename {name:?}). \
Pass one explicitly: bash | zsh | fish | powershell | elvish"
)));
}
return Err(CliError::InvalidArgument(format!(
"could not auto-detect shell from $SHELL={raw:?} (no basename). \
Pass one explicitly: bash | zsh | fish | powershell | elvish"
)));
}
if cfg!(windows) {
return Ok(Shell::PowerShell);
}
Err(CliError::InvalidArgument(
"could not auto-detect shell ($SHELL is unset). \
Pass one explicitly: bash | zsh | fish | powershell | elvish"
.to_owned(),
))
}
fn shell_basename(path: &str) -> Option<&str> {
let last_token = path
.split_ascii_whitespace()
.next_back()
.filter(|s| !s.is_empty())?;
let name = last_token
.rsplit(['/', '\\'])
.next()
.filter(|s| !s.is_empty())?;
let trimmed = if name.len() >= 4 && name[name.len() - 4..].eq_ignore_ascii_case(".exe") {
&name[..name.len() - 4]
} else {
name
};
Some(trimmed)
}
fn map_shell_name(name: &str) -> Option<Shell> {
match name.to_ascii_lowercase().as_str() {
"bash" => Some(Shell::Bash),
"zsh" => Some(Shell::Zsh),
"fish" => Some(Shell::Fish),
"elvish" => Some(Shell::Elvish),
"pwsh" | "powershell" => Some(Shell::PowerShell),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_basename_strips_unix_path() {
assert_eq!(shell_basename("/bin/zsh"), Some("zsh"));
assert_eq!(shell_basename("/usr/local/bin/fish"), Some("fish"));
}
#[test]
fn shell_basename_strips_windows_path_and_exe() {
assert_eq!(
shell_basename(r"C:\Program Files\PowerShell\7\pwsh.exe"),
Some("pwsh"),
);
}
#[test]
fn shell_basename_strips_uppercase_exe() {
assert_eq!(
shell_basename(r"C:\WINDOWS\System32\PWSH.EXE"),
Some("PWSH"),
);
assert_eq!(shell_basename(r"C:\bin\Bash.Exe"), Some("Bash"));
}
#[test]
fn shell_basename_handles_bare_name() {
assert_eq!(shell_basename("zsh"), Some("zsh"));
}
#[test]
fn shell_basename_handles_wrapper_invocation() {
assert_eq!(shell_basename("/usr/bin/script /bin/zsh"), Some("zsh"));
assert_eq!(shell_basename("nice -n 19 /usr/bin/fish"), Some("fish"));
}
#[test]
fn shell_basename_rejects_empty_and_trailing_separator() {
assert_eq!(shell_basename(""), None);
assert_eq!(shell_basename("/bin/"), None);
assert_eq!(shell_basename(" "), None);
}
#[test]
fn map_shell_name_known_shells() {
assert_eq!(map_shell_name("bash"), Some(Shell::Bash));
assert_eq!(map_shell_name("zsh"), Some(Shell::Zsh));
assert_eq!(map_shell_name("fish"), Some(Shell::Fish));
assert_eq!(map_shell_name("elvish"), Some(Shell::Elvish));
assert_eq!(map_shell_name("pwsh"), Some(Shell::PowerShell));
assert_eq!(map_shell_name("powershell"), Some(Shell::PowerShell));
assert_eq!(map_shell_name("PowerShell"), Some(Shell::PowerShell));
}
#[test]
fn map_shell_name_unknown_shell() {
assert_eq!(map_shell_name("nu"), None);
assert_eq!(map_shell_name("xonsh"), None);
assert_eq!(map_shell_name(""), None);
}
#[test]
fn map_shell_name_does_not_assume_sh_is_bash() {
assert_eq!(map_shell_name("sh"), None);
assert_eq!(map_shell_name("dash"), None);
assert_eq!(map_shell_name("ash"), None);
assert_eq!(map_shell_name("ksh"), None);
}
}