seshat_cli/
completions.rs1use std::io::{self, Write};
10
11use clap::CommandFactory;
12use clap_complete::{Shell, generate};
13
14use crate::args::Cli;
15use crate::error::CliError;
16
17const COMPLETION_BIN_NAME: &str = "seshat";
23
24pub fn run_completions(shell: Option<Shell>) -> Result<(), CliError> {
32 let shell = match shell {
33 Some(s) => s,
34 None => detect_shell()?,
35 };
36
37 let mut cmd = Cli::command();
38 let stdout = io::stdout();
39 let mut handle = stdout.lock();
40 generate(shell, &mut cmd, COMPLETION_BIN_NAME, &mut handle);
41
42 match handle.flush() {
46 Ok(()) => Ok(()),
47 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
48 Err(e) => Err(CliError::Io(e)),
49 }
50}
51
52fn detect_shell() -> Result<Shell, CliError> {
61 let raw_set = std::env::var("SHELL").ok();
62 let raw = raw_set
65 .as_deref()
66 .map(|s| s.trim().trim_end_matches('\r'))
67 .filter(|s| !s.is_empty());
68
69 if let Some(raw) = raw {
70 if let Some(name) = shell_basename(raw) {
71 if let Some(shell) = map_shell_name(name) {
72 return Ok(shell);
73 }
74 return Err(CliError::InvalidArgument(format!(
75 "could not auto-detect shell from $SHELL={raw:?} (basename {name:?}). \
76 Pass one explicitly: bash | zsh | fish | powershell | elvish"
77 )));
78 }
79 return Err(CliError::InvalidArgument(format!(
82 "could not auto-detect shell from $SHELL={raw:?} (no basename). \
83 Pass one explicitly: bash | zsh | fish | powershell | elvish"
84 )));
85 }
86
87 if cfg!(windows) {
88 return Ok(Shell::PowerShell);
89 }
90
91 Err(CliError::InvalidArgument(
92 "could not auto-detect shell ($SHELL is unset). \
93 Pass one explicitly: bash | zsh | fish | powershell | elvish"
94 .to_owned(),
95 ))
96}
97
98fn shell_basename(path: &str) -> Option<&str> {
104 let last_token = path
106 .split_ascii_whitespace()
107 .next_back()
108 .filter(|s| !s.is_empty())?;
109 let name = last_token
110 .rsplit(['/', '\\'])
111 .next()
112 .filter(|s| !s.is_empty())?;
113 let trimmed = if name.len() >= 4 && name[name.len() - 4..].eq_ignore_ascii_case(".exe") {
116 &name[..name.len() - 4]
117 } else {
118 name
119 };
120 Some(trimmed)
121}
122
123fn map_shell_name(name: &str) -> Option<Shell> {
133 match name.to_ascii_lowercase().as_str() {
134 "bash" => Some(Shell::Bash),
135 "zsh" => Some(Shell::Zsh),
136 "fish" => Some(Shell::Fish),
137 "elvish" => Some(Shell::Elvish),
138 "pwsh" | "powershell" => Some(Shell::PowerShell),
139 _ => None,
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn shell_basename_strips_unix_path() {
149 assert_eq!(shell_basename("/bin/zsh"), Some("zsh"));
150 assert_eq!(shell_basename("/usr/local/bin/fish"), Some("fish"));
151 }
152
153 #[test]
154 fn shell_basename_strips_windows_path_and_exe() {
155 assert_eq!(
156 shell_basename(r"C:\Program Files\PowerShell\7\pwsh.exe"),
157 Some("pwsh"),
158 );
159 }
160
161 #[test]
162 fn shell_basename_strips_uppercase_exe() {
163 assert_eq!(
167 shell_basename(r"C:\WINDOWS\System32\PWSH.EXE"),
168 Some("PWSH"),
169 );
170 assert_eq!(shell_basename(r"C:\bin\Bash.Exe"), Some("Bash"));
171 }
172
173 #[test]
174 fn shell_basename_handles_bare_name() {
175 assert_eq!(shell_basename("zsh"), Some("zsh"));
176 }
177
178 #[test]
179 fn shell_basename_handles_wrapper_invocation() {
180 assert_eq!(shell_basename("/usr/bin/script /bin/zsh"), Some("zsh"));
184 assert_eq!(shell_basename("nice -n 19 /usr/bin/fish"), Some("fish"));
185 }
186
187 #[test]
188 fn shell_basename_rejects_empty_and_trailing_separator() {
189 assert_eq!(shell_basename(""), None);
190 assert_eq!(shell_basename("/bin/"), None);
191 assert_eq!(shell_basename(" "), None);
192 }
193
194 #[test]
195 fn map_shell_name_known_shells() {
196 assert_eq!(map_shell_name("bash"), Some(Shell::Bash));
197 assert_eq!(map_shell_name("zsh"), Some(Shell::Zsh));
198 assert_eq!(map_shell_name("fish"), Some(Shell::Fish));
199 assert_eq!(map_shell_name("elvish"), Some(Shell::Elvish));
200 assert_eq!(map_shell_name("pwsh"), Some(Shell::PowerShell));
201 assert_eq!(map_shell_name("powershell"), Some(Shell::PowerShell));
202 assert_eq!(map_shell_name("PowerShell"), Some(Shell::PowerShell));
203 }
204
205 #[test]
206 fn map_shell_name_unknown_shell() {
207 assert_eq!(map_shell_name("nu"), None);
208 assert_eq!(map_shell_name("xonsh"), None);
209 assert_eq!(map_shell_name(""), None);
210 }
211
212 #[test]
213 fn map_shell_name_does_not_assume_sh_is_bash() {
214 assert_eq!(map_shell_name("sh"), None);
217 assert_eq!(map_shell_name("dash"), None);
218 assert_eq!(map_shell_name("ash"), None);
219 assert_eq!(map_shell_name("ksh"), None);
220 }
221}