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