1use sysinfo::System;
7
8pub(crate) fn detect_shell(sys: &System) -> Option<String> {
9 let known_shells = [
10 "bash",
11 "zsh",
12 "fish",
13 "sh",
14 "dash",
15 "nu",
16 "elvish",
17 "tcsh",
18 "csh",
19 "ksh",
20 "powershell",
21 "pwsh",
22 "cmd",
23 ];
24
25 let mut current_pid = sysinfo::get_current_pid().ok();
27 let mut detected: Option<(String, String)> = None;
28 while let Some(pid) = current_pid {
29 if let Some(process) = sys.process(pid) {
30 let proc_name = process.name().to_string_lossy().to_string();
31 let proc_name_lower = proc_name.to_lowercase();
32 let clean_name = proc_name_lower
33 .strip_suffix(".exe")
34 .unwrap_or(&proc_name_lower)
35 .to_string();
36 if known_shells.contains(&clean_name.as_str()) {
37 detected = Some((proc_name.clone(), clean_name));
38 break;
39 }
40 current_pid = process.parent();
41 } else {
42 break;
43 }
44 }
45
46 let (shell_path, shell_name) = if let Some((orig_name, clean_name)) = detected {
47 (orig_name, clean_name)
48 } else if let Ok(path_str) = std::env::var("SHELL") {
49 let path = std::path::Path::new(&path_str);
51 let name = path
52 .file_name()
53 .and_then(|n| n.to_str())
54 .unwrap_or(&path_str)
55 .to_string();
56 (path_str, name)
57 } else {
58 #[cfg(target_os = "windows")]
59 {
60 if std::env::var("PSModulePath").is_ok() {
61 ("powershell.exe".to_string(), "powershell".to_string())
62 } else {
63 ("cmd.exe".to_string(), "cmd".to_string())
64 }
65 }
66 #[cfg(not(target_os = "windows"))]
67 {
68 ("sh".to_string(), "sh".to_string())
69 }
70 };
71
72 let shell_name_lower = shell_name.to_lowercase();
73 let shell_name_clean = shell_name_lower
74 .strip_suffix(".exe")
75 .unwrap_or(&shell_name_lower);
76
77 let version = detect_shell_version(&shell_path, shell_name_clean);
78
79 if let Some(ver) = version {
80 Some(format!("{} {}", shell_name_clean, ver))
81 } else {
82 Some(shell_name_clean.to_string())
83 }
84}
85
86fn detect_shell_version(shell_path: &str, shell_name: &str) -> Option<String> {
87 let args = match shell_name {
88 "powershell" => vec![
89 "-NoProfile",
90 "-Command",
91 "$PSVersionTable.PSVersion.ToString()",
92 ],
93 "elvish" => vec!["-version"],
94 _ => vec!["--version"],
95 };
96
97 let output = std::process::Command::new(shell_path)
98 .args(&args)
99 .output()
100 .or_else(|_| std::process::Command::new(shell_name).args(&args).output())
101 .ok()?;
102
103 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
104 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
105 let full_output = format!("{}\n{}", stdout, stderr);
106
107 parse_shell_version(shell_name, &full_output)
108}
109
110pub fn parse_shell_version(shell_name: &str, output: &str) -> Option<String> {
111 let output_trimmed = output.trim();
112 if output_trimmed.is_empty() {
113 return None;
114 }
115
116 match shell_name {
117 "bash" => {
118 if let Some(pos) = output.find("version ") {
119 let rest = &output[pos + 8..];
120 let ver = rest
121 .split(|c: char| c.is_whitespace() || c == '(' || c == ',' || c == '-')
122 .next()
123 .unwrap_or("");
124 if !ver.is_empty() {
125 return Some(ver.to_string());
126 }
127 }
128 }
129 "zsh" => {
130 if let Some(pos) = output.find("zsh ") {
131 let rest = &output[pos + 4..];
132 let ver = rest
133 .split(|c: char| c.is_whitespace() || c == '(')
134 .next()
135 .unwrap_or("");
136 if !ver.is_empty() {
137 return Some(ver.to_string());
138 }
139 }
140 }
141 "fish" => {
142 if let Some(pos) = output.find("version ") {
143 let rest = &output[pos + 8..];
144 let ver = rest
145 .split(|c: char| c.is_whitespace() || c == '(' || c == ',')
146 .next()
147 .unwrap_or("");
148 if !ver.is_empty() {
149 return Some(ver.to_string());
150 }
151 }
152 }
153 "nu" => {
154 let ver = output_trimmed.split_whitespace().next().unwrap_or("");
155 if !ver.is_empty() {
156 return Some(ver.to_string());
157 }
158 }
159 "pwsh" => {
160 if let Some(pos) = output.find("PowerShell ") {
161 let rest = &output[pos + 11..];
162 let ver = rest.split_whitespace().next().unwrap_or("");
163 if !ver.is_empty() {
164 return Some(ver.to_string());
165 }
166 }
167 }
168 "powershell" => {
169 let ver = output_trimmed.split_whitespace().next().unwrap_or("");
170 if !ver.is_empty() {
171 return Some(ver.to_string());
172 }
173 }
174 "elvish" => {
175 let ver = output_trimmed.split_whitespace().next().unwrap_or("");
176 if !ver.is_empty() {
177 return Some(ver.to_string());
178 }
179 }
180 "tcsh" => {
181 if let Some(pos) = output.find("tcsh ") {
182 let rest = &output[pos + 5..];
183 let ver = rest.split_whitespace().next().unwrap_or("");
184 if !ver.is_empty() {
185 return Some(ver.to_string());
186 }
187 }
188 }
189 _ => {
190 if let Some(pos) = output.to_lowercase().find("version ") {
191 let rest = &output[pos + 8..];
192 let ver = rest
193 .split(|c: char| c.is_whitespace() || c == '(' || c == ',' || c == '-')
194 .next()
195 .unwrap_or("");
196 if !ver.is_empty() {
197 return Some(ver.to_string());
198 }
199 }
200 }
201 }
202
203 None
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_parse_shell_version() {
212 let bash_out = "GNU bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)\nCopyright (C) 2022 Free Software Foundation, Inc.";
213 assert_eq!(
214 parse_shell_version("bash", bash_out),
215 Some("5.2.15".to_string())
216 );
217
218 let zsh_out = "zsh 5.9 (x86_64-pc-linux-gnu)";
219 assert_eq!(parse_shell_version("zsh", zsh_out), Some("5.9".to_string()));
220
221 let fish_out = "fish, version 3.6.0";
222 assert_eq!(
223 parse_shell_version("fish", fish_out),
224 Some("3.6.0".to_string())
225 );
226
227 let nu_out = "0.93.0";
228 assert_eq!(
229 parse_shell_version("nu", nu_out),
230 Some("0.93.0".to_string())
231 );
232
233 let pwsh_out = "PowerShell 7.4.1";
234 assert_eq!(
235 parse_shell_version("pwsh", pwsh_out),
236 Some("7.4.1".to_string())
237 );
238
239 let powershell_out = "5.1.22621.2428";
240 assert_eq!(
241 parse_shell_version("powershell", powershell_out),
242 Some("5.1.22621.2428".to_string())
243 );
244
245 let elvish_out = "0.20.1";
246 assert_eq!(
247 parse_shell_version("elvish", elvish_out),
248 Some("0.20.1".to_string())
249 );
250
251 let tcsh_out = "tcsh 6.24.10 (Astron) 2023-04-20 (x86_64-amd-linux) options wide,nls,dl,al,kan,sm,color,filec";
252 assert_eq!(
253 parse_shell_version("tcsh", tcsh_out),
254 Some("6.24.10".to_string())
255 );
256
257 let custom_out = "CustomShell version 1.2.3-patch4";
258 assert_eq!(
259 parse_shell_version("custom", custom_out),
260 Some("1.2.3".to_string())
261 );
262 }
263}