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