1mod runnable;
2mod shlex;
3#[cfg(windows)]
4mod windows;
5
6pub use runnable::WindowsRunnable;
7pub use shlex::{escape_posix_for_single_quotes, shlex_posix, shlex_windows};
8#[cfg(windows)]
9pub use windows::prepend_path;
10
11use std::env::home_dir;
12use std::path::{Path, PathBuf};
13
14use uv_fs::Simplified;
15use uv_static::EnvVars;
16
17#[cfg(unix)]
18use tracing::debug;
19
20#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
22#[expect(clippy::doc_markdown)]
23pub enum Shell {
24 Bash,
26 Fish,
28 Powershell,
30 Cmd,
32 Zsh,
34 Nushell,
36 Csh,
38 Ksh,
40}
41
42impl Shell {
43 pub fn from_env() -> Option<Self> {
55 if std::env::var_os(EnvVars::NU_VERSION).is_some() {
56 Some(Self::Nushell)
57 } else if std::env::var_os(EnvVars::FISH_VERSION).is_some() {
58 Some(Self::Fish)
59 } else if std::env::var_os(EnvVars::BASH_VERSION).is_some() {
60 Some(Self::Bash)
61 } else if std::env::var_os(EnvVars::ZSH_VERSION).is_some() {
62 Some(Self::Zsh)
63 } else if std::env::var_os(EnvVars::KSH_VERSION).is_some() {
64 Some(Self::Ksh)
65 } else if std::env::var_os(EnvVars::PS_MODULE_PATH).is_some() {
66 Some(Self::Powershell)
67 } else if let Some(env_shell) = std::env::var_os(EnvVars::SHELL) {
68 Self::from_shell_path(env_shell)
69 } else if cfg!(windows) {
70 if std::env::var_os(EnvVars::PROMPT).is_some() {
73 Some(Self::Cmd)
74 } else {
75 Some(Self::Powershell)
77 }
78 } else {
79 Self::from_parent_process()
81 }
82 }
83
84 fn from_parent_process() -> Option<Self> {
93 #[cfg(unix)]
94 {
95 let ppid = nix::unistd::getppid();
97 debug!("Detected parent process ID: {ppid}");
98
99 let proc_exe_path = format!("/proc/{ppid}/exe");
101 if let Ok(exe_path) = fs_err::read_link(&proc_exe_path) {
102 debug!("Parent process executable: {}", exe_path.display());
103 if let Some(shell) = Self::from_shell_path(&exe_path) {
104 return Some(shell);
105 }
106 }
107
108 let proc_comm_path = format!("/proc/{ppid}/comm");
110 if let Ok(comm) = fs_err::read_to_string(&proc_comm_path) {
111 let comm = comm.trim();
112 debug!("Parent process comm: {comm}");
113 if let Some(shell) = parse_shell_from_path(Path::new(comm)) {
114 return Some(shell);
115 }
116 }
117
118 debug!("Could not determine shell from parent process");
119 None
120 }
121
122 #[cfg(not(unix))]
123 {
124 None
125 }
126 }
127
128 fn from_shell_path(path: impl AsRef<Path>) -> Option<Self> {
140 parse_shell_from_path(path.as_ref())
141 }
142
143 pub fn supports_update(self) -> bool {
145 match self {
146 Self::Powershell | Self::Cmd => true,
147 shell => !shell.configuration_files().is_empty(),
148 }
149 }
150
151 pub fn configuration_files(self) -> Vec<PathBuf> {
157 let Some(home_dir) = home_dir() else {
158 return vec![];
159 };
160 match self {
161 Self::Bash => {
162 vec![
168 [".bash_profile", ".bash_login", ".profile"]
169 .iter()
170 .map(|rc| home_dir.join(rc))
171 .find(|rc| rc.is_file())
172 .unwrap_or_else(|| home_dir.join(".bash_profile")),
173 home_dir.join(".bashrc"),
174 ]
175 }
176 Self::Ksh => {
177 vec![home_dir.join(".profile"), home_dir.join(".kshrc")]
179 }
180 Self::Zsh => {
181 let zsh_dot_dir = std::env::var(EnvVars::ZDOTDIR)
187 .ok()
188 .filter(|dir| !dir.is_empty())
189 .map(PathBuf::from);
190
191 if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
193 let zshenv = zsh_dot_dir.join(".zshenv");
195 if zshenv.is_file() {
196 return vec![zshenv];
197 }
198 }
199 let zshenv = home_dir.join(".zshenv");
201 if zshenv.is_file() {
202 return vec![zshenv];
203 }
204
205 if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
206 vec![zsh_dot_dir.join(".zshenv")]
208 } else {
209 vec![home_dir.join(".zshenv")]
211 }
212 }
213 Self::Fish => {
214 if let Some(xdg_home_dir) = std::env::var(EnvVars::XDG_CONFIG_HOME)
219 .ok()
220 .filter(|dir| !dir.is_empty())
221 .map(PathBuf::from)
222 {
223 vec![xdg_home_dir.join("fish/config.fish")]
224 } else {
225 vec![home_dir.join(".config/fish/config.fish")]
226 }
227 }
228 Self::Csh => {
229 vec![home_dir.join(".cshrc"), home_dir.join(".login")]
231 }
232 Self::Nushell => vec![],
234 Self::Powershell => vec![],
236 Self::Cmd => vec![],
238 }
239 }
240
241 pub fn contains_path(path: &Path) -> bool {
243 let home_dir = home_dir();
244 std::env::var_os(EnvVars::PATH)
245 .as_ref()
246 .iter()
247 .flat_map(std::env::split_paths)
248 .map(|path| {
249 if let Some(home_dir) = home_dir.as_ref() {
251 if path
252 .components()
253 .next()
254 .map(std::path::Component::as_os_str)
255 == Some("~".as_ref())
256 {
257 return home_dir.join(path.components().skip(1).collect::<PathBuf>());
258 }
259 }
260 path
261 })
262 .any(|p| same_file::is_same_file(path, p).unwrap_or(false))
263 }
264
265 pub fn prepend_path(self, path: &Path) -> Option<String> {
267 match self {
268 Self::Nushell => None,
269 Self::Bash | Self::Zsh | Self::Ksh => Some(format!(
270 "export PATH=\"{}:$PATH\"",
271 backslash_escape(&path.simplified_display().to_string()),
272 )),
273 Self::Fish => Some(format!(
274 "fish_add_path \"{}\"",
275 backslash_escape(&path.simplified_display().to_string()),
276 )),
277 Self::Csh => Some(format!(
278 "setenv PATH \"{}:$PATH\"",
279 backslash_escape(&path.simplified_display().to_string()),
280 )),
281 Self::Powershell => Some(format!(
282 "$env:PATH = \"{};$env:PATH\"",
283 backtick_escape(&path.simplified_display().to_string()),
284 )),
285 Self::Cmd => Some(format!(
286 "set PATH=\"{};%PATH%\"",
287 backslash_escape(&path.simplified_display().to_string()),
288 )),
289 }
290 }
291}
292
293impl std::fmt::Display for Shell {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 Self::Bash => write!(f, "Bash"),
297 Self::Fish => write!(f, "Fish"),
298 Self::Powershell => write!(f, "PowerShell"),
299 Self::Cmd => write!(f, "Command Prompt"),
300 Self::Zsh => write!(f, "Zsh"),
301 Self::Nushell => write!(f, "Nushell"),
302 Self::Csh => write!(f, "Csh"),
303 Self::Ksh => write!(f, "Ksh"),
304 }
305 }
306}
307
308fn parse_shell_from_path(path: &Path) -> Option<Shell> {
310 let name = path.file_stem()?.to_str()?;
311 match name {
312 "bash" => Some(Shell::Bash),
313 "zsh" => Some(Shell::Zsh),
314 "fish" => Some(Shell::Fish),
315 "csh" => Some(Shell::Csh),
316 "ksh" => Some(Shell::Ksh),
317 "powershell" | "powershell_ise" | "pwsh" => Some(Shell::Powershell),
318 _ => None,
319 }
320}
321
322fn backslash_escape(s: &str) -> String {
324 let mut escaped = String::with_capacity(s.len());
325 for c in s.chars() {
326 match c {
327 '\\' | '"' => escaped.push('\\'),
328 _ => {}
329 }
330 escaped.push(c);
331 }
332 escaped
333}
334
335fn backtick_escape(s: &str) -> String {
337 let mut escaped = String::with_capacity(s.len());
338 for c in s.chars() {
339 match c {
340 '"' | '`' | '\u{201C}' | '\u{201D}' | '\u{201E}' | '$' => escaped.push('`'),
343 _ => {}
344 }
345 escaped.push(c);
346 }
347 escaped
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use fs_err::File;
354 use temp_env::with_vars;
355 use tempfile::tempdir;
356
357 const HOME_DIR_ENV_VAR: &str = if cfg!(windows) {
359 EnvVars::USERPROFILE
360 } else {
361 EnvVars::HOME
362 };
363
364 #[test]
365 fn configuration_files_zsh_no_existing_zshenv() {
366 let tmp_home_dir = tempdir().unwrap();
367 let tmp_zdotdir = tempdir().unwrap();
368
369 with_vars(
370 [
371 (EnvVars::ZDOTDIR, None),
372 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
373 ],
374 || {
375 assert_eq!(
376 Shell::Zsh.configuration_files(),
377 vec![tmp_home_dir.path().join(".zshenv")]
378 );
379 },
380 );
381
382 with_vars(
383 [
384 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
385 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
386 ],
387 || {
388 assert_eq!(
389 Shell::Zsh.configuration_files(),
390 vec![tmp_zdotdir.path().join(".zshenv")]
391 );
392 },
393 );
394 }
395
396 #[test]
397 fn configuration_files_zsh_existing_home_zshenv() {
398 let tmp_home_dir = tempdir().unwrap();
399 File::create(tmp_home_dir.path().join(".zshenv")).unwrap();
400
401 let tmp_zdotdir = tempdir().unwrap();
402
403 with_vars(
404 [
405 (EnvVars::ZDOTDIR, None),
406 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
407 ],
408 || {
409 assert_eq!(
410 Shell::Zsh.configuration_files(),
411 vec![tmp_home_dir.path().join(".zshenv")]
412 );
413 },
414 );
415
416 with_vars(
417 [
418 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
419 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
420 ],
421 || {
422 assert_eq!(
423 Shell::Zsh.configuration_files(),
424 vec![tmp_home_dir.path().join(".zshenv")]
425 );
426 },
427 );
428 }
429
430 #[test]
431 fn configuration_files_zsh_existing_zdotdir_zshenv() {
432 let tmp_home_dir = tempdir().unwrap();
433
434 let tmp_zdotdir = tempdir().unwrap();
435 File::create(tmp_zdotdir.path().join(".zshenv")).unwrap();
436
437 with_vars(
438 [
439 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
440 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
441 ],
442 || {
443 assert_eq!(
444 Shell::Zsh.configuration_files(),
445 vec![tmp_zdotdir.path().join(".zshenv")]
446 );
447 },
448 );
449 }
450}