1use regex::Regex;
2use std::fmt::Display;
3use std::{ffi::OsStr, process::Command};
4
5#[cfg(unix)]
6mod unix;
7#[cfg(unix)]
8use unix::*;
9
10#[cfg(windows)]
11mod windows;
12#[cfg(windows)]
13use windows::*;
14
15fn exec<I, S>(cmd: S, args: I) -> Option<String>
16where
17 I: IntoIterator<Item = S>,
18 S: AsRef<OsStr>,
19{
20 let output = Command::new(cmd)
21 .args(args)
22 .envs(std::env::vars())
23 .output()
24 .ok()?;
25 let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
26 Some(s)
27}
28
29fn get_file_name(path: &str) -> Option<String> {
30 let path = path.replace('\\', "/");
31 let name = path.split('/').next_back()?.split('.').next()?.trim();
32 Some(name.into())
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum Shell {
37 Bash,
38 Zsh,
39 Fish,
40 PowerShell,
41 Pwsh,
42 Cmd,
43 Nu,
44 Dash,
45 Ksh,
46 Tcsh,
47 Csh,
48 Sh,
49 Unknown,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub struct ShellVersion {
54 pub shell: Shell,
55 pub version: Option<String>,
56}
57
58impl Display for ShellVersion {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 if let Some(ref v) = self.version {
61 f.write_str(&format!("{} {}", self.shell, v))
62 } else {
63 f.write_str(&format!("{}", self.shell))
64 }
65 }
66}
67
68impl From<&str> for Shell {
69 fn from(val: &str) -> Self {
70 match val {
71 "fish" => Shell::Fish,
72 "zsh" => Shell::Zsh,
73 "OpenConsole" => Shell::PowerShell,
74 "powershell" => Shell::PowerShell,
75 "bash" => Shell::Bash,
76 "pwsh" => Shell::Pwsh,
77 "cmd" => Shell::Cmd,
78 "nu" => Shell::Nu,
79 "dash" => Shell::Dash,
80 "ksh" => Shell::Ksh,
81 "ksh93" => Shell::Ksh,
82 "tcsh" => Shell::Tcsh,
83 "csh" => Shell::Csh,
84 "bsd-csh" => Shell::Csh,
85 "sh" => Shell::Sh,
86 _ => Shell::Unknown,
87 }
88 }
89}
90
91impl Display for Shell {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 let s = match self {
94 Shell::Fish => "fish",
95 Shell::Zsh => "zsh",
96 Shell::Bash => "bash",
97 Shell::PowerShell => "powershell",
98 Shell::Cmd => "cmd",
99 Shell::Pwsh => "pwsh",
100 Shell::Nu => "nu",
101 Shell::Dash => "dash",
102 Shell::Ksh => "ksh",
103 Shell::Tcsh => "tcsh",
104 Shell::Csh => "csh",
105 Shell::Sh => "sh",
106 Shell::Unknown => "unknown",
107 };
108 f.write_str(s)
109 }
110}
111
112fn get_shell_version(sh: Shell) -> Option<String> {
113 let args = match sh {
114 Shell::PowerShell => vec!["-c", "$PSVersionTable.PSVersion -replace '\\D', '.'"],
115 Shell::Ksh => vec!["-c", "echo $KSH_VERSION"],
116 _ => vec!["--version"],
117 };
118 let version = exec(sh.to_string().as_str(), args)?;
119 match sh {
120 Shell::Fish => {
121 Some(version[14..].trim().into())
123 }
124 Shell::Pwsh => {
125 Some(version[11..].trim().into())
127 }
128 Shell::Bash => {
129 let re = Regex::new(r"([0-9]+).([0-9]+).([0-9]+)").unwrap();
131 let cap = re.captures(&version)?;
132
133 if let (Some(a), Some(b), Some(c)) = (cap.get(1), cap.get(2), cap.get(3)) {
134 return Some(format!("{}.{}.{}", a.as_str(), b.as_str(), c.as_str()));
135 }
136 None
137 }
138 Shell::Cmd => {
139 let s = version
142 .lines()
143 .next()?
144 .split(' ')
145 .next_back()?
146 .split(']')
147 .next()?
148 .trim();
149 Some(s.into())
150 }
151 Shell::PowerShell => {
152 Some(version)
154 }
155 Shell::Nu => {
156 Some(version)
158 }
159 Shell::Ksh => {
160 let v = version.split("/").nth(1)?;
162 let v = v.split(" ").next().map(|s| s.trim().to_string());
163 v
164 }
165 Shell::Zsh => {
166 let v = version.split(" ").nth(1).map(|s| s.trim().to_string());
168 v
169 }
170 Shell::Tcsh => {
171 let v = version.split(" ").nth(1).map(|s| s.trim().to_string());
173 v
174 }
175 _ => None,
176 }
177}
178
179pub fn which_shell() -> Option<ShellVersion> {
180 let mut pid = std::process::id();
181 while let Some((ppid, path)) = get_ppid(pid) {
182 let cmd = get_file_name(&path)?;
183 let shell: Shell = cmd.as_str().into();
184 match shell {
185 Shell::Unknown => {
186 pid = ppid;
187 continue;
188 }
189 _ => {
190 let version = get_shell_version(shell);
191 return Some(ShellVersion { shell, version });
192 }
193 }
194 }
195 None
196}