1use std::path::PathBuf;
2
3use serde::Deserialize;
4use serde::Serialize;
5
6#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
7pub enum ShellType {
8 Zsh,
9 Bash,
10 PowerShell,
11 Sh,
12 Cmd,
13}
14
15impl ShellType {
16 pub fn name(self) -> &'static str {
17 match self {
18 Self::Zsh => "zsh",
19 Self::Bash => "bash",
20 Self::PowerShell => "powershell",
21 Self::Sh => "sh",
22 Self::Cmd => "cmd",
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct DetectedShell {
29 pub shell_type: ShellType,
30 pub shell_path: PathBuf,
31}
32
33impl DetectedShell {
34 pub fn name(&self) -> &'static str {
35 self.shell_type.name()
36 }
37}
38
39pub fn detect_shell_type(shell_path: impl AsRef<std::path::Path>) -> Option<ShellType> {
40 let shell_path = shell_path.as_ref();
41 match shell_path.as_os_str().to_str() {
42 Some("zsh") => Some(ShellType::Zsh),
43 Some("sh") => Some(ShellType::Sh),
44 Some("cmd") => Some(ShellType::Cmd),
45 Some("bash") => Some(ShellType::Bash),
46 Some("pwsh") => Some(ShellType::PowerShell),
47 Some("powershell") => Some(ShellType::PowerShell),
48 _ => {
49 let shell_name = shell_path.file_stem();
50 if let Some(shell_name) = shell_name {
51 let shell_name_path = std::path::Path::new(shell_name);
52 if shell_name_path != shell_path {
53 return detect_shell_type(shell_name_path);
54 }
55 }
56 None
57 }
58 }
59}
60
61#[cfg(unix)]
62fn get_user_shell_path() -> Option<PathBuf> {
63 let uid = unsafe { libc::getuid() };
64 use std::ffi::CStr;
65 use std::mem::MaybeUninit;
66 use std::ptr;
67
68 let mut passwd = MaybeUninit::<libc::passwd>::uninit();
69
70 let suggested_buffer_len = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
75 let buffer_len = usize::try_from(suggested_buffer_len)
76 .ok()
77 .filter(|len| *len > 0)
78 .unwrap_or(1024);
79 let mut buffer = vec![0; buffer_len];
80
81 loop {
82 let mut result = ptr::null_mut();
83 let status = unsafe {
84 libc::getpwuid_r(
85 uid,
86 passwd.as_mut_ptr(),
87 buffer.as_mut_ptr().cast(),
88 buffer.len(),
89 &mut result,
90 )
91 };
92
93 if status == 0 {
94 if result.is_null() {
95 return None;
96 }
97
98 let passwd = unsafe { passwd.assume_init_ref() };
99 if passwd.pw_shell.is_null() {
100 return None;
101 }
102
103 let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) }
104 .to_string_lossy()
105 .into_owned();
106 return Some(PathBuf::from(shell_path));
107 }
108
109 if status != libc::ERANGE {
110 return None;
111 }
112
113 let new_len = buffer.len().checked_mul(2)?;
115 if new_len > 1024 * 1024 {
116 return None;
117 }
118 buffer.resize(new_len, 0);
119 }
120}
121
122#[cfg(not(unix))]
123fn get_user_shell_path() -> Option<PathBuf> {
124 None
125}
126
127fn file_exists(path: &std::path::Path) -> Option<PathBuf> {
128 if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) {
129 Some(PathBuf::from(path))
130 } else {
131 None
132 }
133}
134
135fn get_shell_path(
136 shell_type: ShellType,
137 provided_path: Option<&PathBuf>,
138 binary_name: &str,
139 fallback_paths: &[&str],
140) -> Option<PathBuf> {
141 if let Some(path) = provided_path.and_then(|path| file_exists(path)) {
142 return Some(path);
143 }
144
145 let default_shell_path = get_user_shell_path();
146 if let Some(default_shell_path) = default_shell_path
147 && detect_shell_type(&default_shell_path) == Some(shell_type)
148 && file_exists(&default_shell_path).is_some()
149 {
150 return Some(default_shell_path);
151 }
152
153 if let Ok(path) = which::which(binary_name) {
154 return Some(path);
155 }
156
157 for path in fallback_paths {
158 if let Some(path) = file_exists(std::path::Path::new(path)) {
159 return Some(path);
160 }
161 }
162
163 None
164}
165
166const ZSH_FALLBACK_PATHS: &[&str] = &["/bin/zsh"];
167
168fn get_zsh_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
169 let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", ZSH_FALLBACK_PATHS);
170
171 shell_path.map(|shell_path| DetectedShell {
172 shell_type: ShellType::Zsh,
173 shell_path,
174 })
175}
176
177const BASH_FALLBACK_PATHS: &[&str] = &["/bin/bash", "/usr/bin/bash"];
178
179fn get_bash_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
180 let shell_path = get_shell_path(ShellType::Bash, path, "bash", BASH_FALLBACK_PATHS);
181
182 shell_path.map(|shell_path| DetectedShell {
183 shell_type: ShellType::Bash,
184 shell_path,
185 })
186}
187
188const SH_FALLBACK_PATHS: &[&str] = &["/bin/sh"];
189
190fn get_sh_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
191 let shell_path = get_shell_path(ShellType::Sh, path, "sh", SH_FALLBACK_PATHS);
192
193 shell_path.map(|shell_path| DetectedShell {
194 shell_type: ShellType::Sh,
195 shell_path,
196 })
197}
198
199#[cfg(windows)]
205const PWSH_FALLBACK_PATHS: &[&str] = &[r#"C:\Program Files\PowerShell\7\pwsh.exe"#];
206#[cfg(not(windows))]
207const PWSH_FALLBACK_PATHS: &[&str] = &["/usr/local/bin/pwsh"];
208
209#[cfg(windows)]
210const POWERSHELL_FALLBACK_PATHS: &[&str] =
211 &[r#"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"#];
212#[cfg(not(windows))]
213const POWERSHELL_FALLBACK_PATHS: &[&str] = &[];
214
215fn get_powershell_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
216 let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS)
217 .or_else(|| {
218 get_shell_path(
219 ShellType::PowerShell,
220 path,
221 "powershell",
222 POWERSHELL_FALLBACK_PATHS,
223 )
224 });
225
226 shell_path.map(|shell_path| DetectedShell {
227 shell_type: ShellType::PowerShell,
228 shell_path,
229 })
230}
231
232fn get_cmd_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
233 let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", &[]);
234
235 shell_path.map(|shell_path| DetectedShell {
236 shell_type: ShellType::Cmd,
237 shell_path,
238 })
239}
240
241pub fn ultimate_fallback_shell() -> DetectedShell {
242 if cfg!(windows) {
243 DetectedShell {
244 shell_type: ShellType::Cmd,
245 shell_path: PathBuf::from("cmd.exe"),
246 }
247 } else {
248 DetectedShell {
249 shell_type: ShellType::Sh,
250 shell_path: PathBuf::from("/bin/sh"),
251 }
252 }
253}
254
255pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> DetectedShell {
256 detect_shell_type(shell_path)
257 .and_then(|shell_type| get_shell(shell_type, Some(shell_path)))
258 .unwrap_or_else(ultimate_fallback_shell)
259}
260
261pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<DetectedShell> {
262 match shell_type {
263 ShellType::Zsh => get_zsh_shell(path),
264 ShellType::Bash => get_bash_shell(path),
265 ShellType::PowerShell => get_powershell_shell(path),
266 ShellType::Sh => get_sh_shell(path),
267 ShellType::Cmd => get_cmd_shell(path),
268 }
269}
270
271pub fn default_user_shell() -> DetectedShell {
272 default_user_shell_from_path(get_user_shell_path())
273}
274
275pub fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> DetectedShell {
276 if cfg!(windows) {
277 get_shell(ShellType::PowerShell, None).unwrap_or_else(ultimate_fallback_shell)
278 } else {
279 let user_default_shell = user_shell_path
280 .and_then(|shell| detect_shell_type(&shell))
281 .and_then(|shell_type| get_shell(shell_type, None));
282
283 let shell_with_fallback = if cfg!(target_os = "macos") {
284 user_default_shell
285 .or_else(|| get_shell(ShellType::Zsh, None))
286 .or_else(|| get_shell(ShellType::Bash, None))
287 } else {
288 user_default_shell
289 .or_else(|| get_shell(ShellType::Bash, None))
290 .or_else(|| get_shell(ShellType::Zsh, None))
291 };
292
293 shell_with_fallback.unwrap_or_else(ultimate_fallback_shell)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use pretty_assertions::assert_eq;
301
302 #[test]
303 fn test_detect_shell_type() {
304 assert_eq!(
305 detect_shell_type(PathBuf::from("zsh")),
306 Some(ShellType::Zsh)
307 );
308 assert_eq!(
309 detect_shell_type(PathBuf::from("bash")),
310 Some(ShellType::Bash)
311 );
312 assert_eq!(
313 detect_shell_type(PathBuf::from("pwsh")),
314 Some(ShellType::PowerShell)
315 );
316 assert_eq!(
317 detect_shell_type(PathBuf::from("powershell")),
318 Some(ShellType::PowerShell)
319 );
320 assert_eq!(detect_shell_type(PathBuf::from("fish")), None);
321 assert_eq!(detect_shell_type(PathBuf::from("other")), None);
322 assert_eq!(
323 detect_shell_type(PathBuf::from("/bin/zsh")),
324 Some(ShellType::Zsh)
325 );
326 assert_eq!(
327 detect_shell_type(PathBuf::from("/bin/bash")),
328 Some(ShellType::Bash)
329 );
330 assert_eq!(
331 detect_shell_type(PathBuf::from("/usr/bin/bash")),
332 Some(ShellType::Bash)
333 );
334 assert_eq!(
335 detect_shell_type(PathBuf::from("powershell.exe")),
336 Some(ShellType::PowerShell)
337 );
338 assert_eq!(
339 detect_shell_type(PathBuf::from(if cfg!(windows) {
340 "C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
341 } else {
342 "/usr/local/bin/pwsh"
343 })),
344 Some(ShellType::PowerShell)
345 );
346 assert_eq!(
347 detect_shell_type(PathBuf::from("pwsh.exe")),
348 Some(ShellType::PowerShell)
349 );
350 assert_eq!(
351 detect_shell_type(PathBuf::from("/usr/local/bin/pwsh")),
352 Some(ShellType::PowerShell)
353 );
354 assert_eq!(
355 detect_shell_type(PathBuf::from("/bin/sh")),
356 Some(ShellType::Sh)
357 );
358 assert_eq!(detect_shell_type(PathBuf::from("sh")), Some(ShellType::Sh));
359 assert_eq!(
360 detect_shell_type(PathBuf::from("cmd")),
361 Some(ShellType::Cmd)
362 );
363 assert_eq!(
364 detect_shell_type(PathBuf::from("cmd.exe")),
365 Some(ShellType::Cmd)
366 );
367 }
368}