which_shell/
lib.rs

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            // fish, version 3.6.1
122            Some(version[14..].trim().into())
123        }
124        Shell::Pwsh => {
125            // PowerShell 7.4.1
126            Some(version[11..].trim().into())
127        }
128        Shell::Bash => {
129            // GNU bash, version 5.2.26(1)-release (aarch64-unknown-linux-android)
130            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            // Microsoft Windows [版本 10.0.22635.2700]
140            // (c) Microsoft Corporation。保留所有权利。
141            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            // 5.1.26100.2161
153            Some(version)
154        }
155        Shell::Nu => {
156            // 0.99.0
157            Some(version)
158        }
159        Shell::Ksh => {
160            // Version AJM 93u+m/1.0.8 2024-01-01
161            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            // zsh 5.9 (x86_64-ubuntu-linux-gnu)
167            let v = version.split(" ").nth(1).map(|s| s.trim().to_string());
168            v
169        }
170        Shell::Tcsh => {
171            // tcsh 6.24.13 (Astron) 2024-06-12 (x86_64-unknown-linux) options wide,nls,dl,al,kan,sm,rh,nd,color,filec
172            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}