leenfetch_core/modules/linux/
shell.rs

1use std::env;
2use std::path::Path;
3use std::process::Command;
4
5/// Get the current shell name and version.
6///
7/// If `show_path` is true, the full path to the shell executable will be included.
8/// If `show_version` is true, the version of the shell will be included.
9///
10/// Returns `None` if the SHELL environment variable is not set.
11///
12/// # Examples
13///
14///
15pub fn get_shell(show_path: bool, show_version: bool) -> Option<String> {
16    let shell_path = env::var("SHELL").ok()?;
17    let shell_name = Path::new(&shell_path).file_name()?.to_string_lossy();
18
19    let mut shell = if show_path {
20        format!("{} ", shell_path)
21    } else {
22        format!("{} ", shell_name)
23    };
24
25    if !show_version {
26        return Some(shell.trim_end().to_string());
27    }
28
29    let version = match shell_name.as_ref() {
30        "bash" => {
31            // Try BASH_VERSION or fallback to subprocess
32            env::var("BASH_VERSION")
33                .ok()
34                .or_else(|| run_version_var(&shell_path, "BASH_VERSION"))
35                .map(|v| v.split('-').next().unwrap_or("").to_string())
36        }
37        "zsh" => run_version_string(&shell_path),
38        "fish" => run_version_arg(&shell_path, "--version"),
39        "nu" => run_nu_version(&shell_path),
40        "yash" => run_yash_version(&shell_path),
41        "tcsh" => run_version_var(&shell_path, "tcsh"),
42        _ => run_version_arg(&shell_path, "--version"),
43    };
44
45    if let Some(ver) = version {
46        shell.push_str(&ver);
47    }
48
49    Some(clean_shell_string(shell))
50}
51
52fn run_version_var(shell_path: &str, var: &str) -> Option<String> {
53    Command::new(shell_path)
54        .arg("-c")
55        .arg(format!("printf %s \"${}\"", var))
56        .output()
57        .ok()
58        .filter(|o| o.status.success())
59        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
60}
61
62fn run_version_arg(shell_path: &str, arg: &str) -> Option<String> {
63    Command::new(shell_path)
64        .arg(arg)
65        .output()
66        .ok()
67        .filter(|o| o.status.success())
68        .map(|o| {
69            let s = String::from_utf8_lossy(&o.stdout);
70            s.lines().next().unwrap_or("").trim().to_string()
71        })
72}
73
74fn run_version_string(shell_path: &str) -> Option<String> {
75    let ver = run_version_arg(shell_path, "--version")?;
76    Some(
77        ver.split_whitespace()
78            .find(|part| part.chars().next().unwrap_or(' ').is_numeric())
79            .unwrap_or("")
80            .to_string(),
81    )
82}
83
84fn run_nu_version(shell_path: &str) -> Option<String> {
85    Command::new(shell_path)
86        .arg("-c")
87        .arg("version | get version")
88        .output()
89        .ok()
90        .filter(|o| o.status.success())
91        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
92}
93
94fn run_yash_version(shell_path: &str) -> Option<String> {
95    let out = Command::new(shell_path)
96        .arg("--version")
97        .output()
98        .ok()?
99        .stdout;
100    let raw = String::from_utf8_lossy(&out);
101    let cleaned = raw
102        .replace("yash", "")
103        .replace("Yet another shell", "")
104        .lines()
105        .next()
106        .unwrap_or("")
107        .trim()
108        .to_string();
109    Some(cleaned)
110}
111
112fn clean_shell_string(s: String) -> String {
113    s.replace(", version", "")
114        .replace("xonsh/", "xonsh ")
115        .replace("options", "")
116        .split('(')
117        .next()
118        .unwrap_or("")
119        .trim()
120        .to_string()
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::test_utils::EnvLock;
127    use std::fs;
128    use std::os::unix::fs::PermissionsExt;
129    use std::time::{SystemTime, UNIX_EPOCH};
130
131    fn create_fake_shell() -> std::path::PathBuf {
132        let unique = SystemTime::now()
133            .duration_since(UNIX_EPOCH)
134            .unwrap()
135            .as_nanos();
136        let path = std::env::temp_dir().join(format!("leenfetch_fake_shell_{unique}"));
137        let script = "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n  echo \"FakeShell 7.8.9\"\nelse\n  echo \"FakeShell\"\nfi\n";
138        fs::write(&path, script).unwrap();
139        let mut perms = fs::metadata(&path).unwrap().permissions();
140        perms.set_mode(0o755);
141        fs::set_permissions(&path, perms).unwrap();
142        path
143    }
144
145    #[test]
146    fn test_shell_no_version() {
147        let script = create_fake_shell();
148        let env_lock = EnvLock::acquire(&["SHELL"]);
149        env_lock.set_var("SHELL", script.to_str().unwrap());
150
151        let shell = get_shell(false, false).expect("expected shell string");
152        assert_eq!(
153            shell,
154            script.file_name().unwrap().to_string_lossy().to_string()
155        );
156
157        drop(env_lock);
158        fs::remove_file(script).unwrap();
159    }
160
161    #[test]
162    fn test_shell_path_on() {
163        let script = create_fake_shell();
164        let env_lock = EnvLock::acquire(&["SHELL"]);
165        env_lock.set_var("SHELL", script.to_str().unwrap());
166
167        let shell = get_shell(true, false).expect("expected shell string");
168        assert_eq!(shell, script.to_str().unwrap());
169
170        drop(env_lock);
171        fs::remove_file(script).unwrap();
172    }
173
174    #[test]
175    fn test_shell_version_optional() {
176        let script = create_fake_shell();
177        let env_lock = EnvLock::acquire(&["SHELL"]);
178        env_lock.set_var("SHELL", script.to_str().unwrap());
179
180        let shell = get_shell(false, true).expect("expected shell string");
181        assert!(
182            shell.contains("7.8.9"),
183            "expected version in output, got {shell}"
184        );
185
186        drop(env_lock);
187        fs::remove_file(script).unwrap();
188    }
189
190    #[test]
191    fn clean_shell_string_strips_noise() {
192        let raw = "bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)";
193        let cleaned = clean_shell_string(raw.to_string());
194        assert_eq!(cleaned, "bash 5.2.15");
195    }
196
197    #[test]
198    fn clean_shell_string_handles_xonsh() {
199        let raw = "xonsh/1.2.3 options [something]";
200        let cleaned = clean_shell_string(raw.to_string());
201        assert!(cleaned.starts_with("xonsh 1.2.3"));
202        assert!(!cleaned.contains("options"));
203        assert!(!cleaned.contains("xonsh/"));
204    }
205}