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