leenfetch_core/modules/linux/
shell.rs1use std::env;
2use std::path::Path;
3use std::process::Command;
4
5pub 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 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}