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> {
50 if std::env::var_os(EnvVars::NU_VERSION).is_some() {
51 Some(Self::Nushell)
52 } else if std::env::var_os(EnvVars::FISH_VERSION).is_some() {
53 Some(Self::Fish)
54 } else if std::env::var_os(EnvVars::BASH_VERSION).is_some() {
55 Some(Self::Bash)
56 } else if std::env::var_os(EnvVars::ZSH_VERSION).is_some() {
57 Some(Self::Zsh)
58 } else if std::env::var_os(EnvVars::KSH_VERSION).is_some() {
59 Some(Self::Ksh)
60 } else if let Some(env_shell) = std::env::var_os(EnvVars::SHELL) {
61 Self::from_shell_path(env_shell)
62 } else if cfg!(windows) {
63 if std::env::var_os(EnvVars::PROMPT).is_some() {
66 Some(Self::Cmd)
67 } else {
68 Some(Self::Powershell)
70 }
71 } else {
72 Self::from_parent_process()
74 }
75 }
76
77 fn from_parent_process() -> Option<Self> {
86 #[cfg(unix)]
87 {
88 let ppid = nix::unistd::getppid();
90 debug!("Detected parent process ID: {ppid}");
91
92 let proc_exe_path = format!("/proc/{ppid}/exe");
94 if let Ok(exe_path) = fs_err::read_link(&proc_exe_path) {
95 debug!("Parent process executable: {}", exe_path.display());
96 if let Some(shell) = Self::from_shell_path(&exe_path) {
97 return Some(shell);
98 }
99 }
100
101 let proc_comm_path = format!("/proc/{ppid}/comm");
103 if let Ok(comm) = fs_err::read_to_string(&proc_comm_path) {
104 let comm = comm.trim();
105 debug!("Parent process comm: {comm}");
106 if let Some(shell) = parse_shell_from_path(Path::new(comm)) {
107 return Some(shell);
108 }
109 }
110
111 debug!("Could not determine shell from parent process");
112 None
113 }
114
115 #[cfg(not(unix))]
116 {
117 None
118 }
119 }
120
121 pub fn from_shell_path(path: impl AsRef<Path>) -> Option<Self> {
133 parse_shell_from_path(path.as_ref())
134 }
135
136 pub fn supports_update(self) -> bool {
138 match self {
139 Self::Powershell | Self::Cmd => true,
140 shell => !shell.configuration_files().is_empty(),
141 }
142 }
143
144 pub fn configuration_files(self) -> Vec<PathBuf> {
150 let Some(home_dir) = home_dir() else {
151 return vec![];
152 };
153 match self {
154 Self::Bash => {
155 vec![
161 [".bash_profile", ".bash_login", ".profile"]
162 .iter()
163 .map(|rc| home_dir.join(rc))
164 .find(|rc| rc.is_file())
165 .unwrap_or_else(|| home_dir.join(".bash_profile")),
166 home_dir.join(".bashrc"),
167 ]
168 }
169 Self::Ksh => {
170 vec![home_dir.join(".profile"), home_dir.join(".kshrc")]
172 }
173 Self::Zsh => {
174 let zsh_dot_dir = std::env::var(EnvVars::ZDOTDIR)
180 .ok()
181 .filter(|dir| !dir.is_empty())
182 .map(PathBuf::from);
183
184 if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
186 let zshenv = zsh_dot_dir.join(".zshenv");
188 if zshenv.is_file() {
189 return vec![zshenv];
190 }
191 }
192 let zshenv = home_dir.join(".zshenv");
194 if zshenv.is_file() {
195 return vec![zshenv];
196 }
197
198 if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
199 vec![zsh_dot_dir.join(".zshenv")]
201 } else {
202 vec![home_dir.join(".zshenv")]
204 }
205 }
206 Self::Fish => {
207 if let Some(xdg_home_dir) = std::env::var(EnvVars::XDG_CONFIG_HOME)
212 .ok()
213 .filter(|dir| !dir.is_empty())
214 .map(PathBuf::from)
215 {
216 vec![xdg_home_dir.join("fish/config.fish")]
217 } else {
218 vec![home_dir.join(".config/fish/config.fish")]
219 }
220 }
221 Self::Csh => {
222 vec![home_dir.join(".cshrc"), home_dir.join(".login")]
224 }
225 Self::Nushell => vec![],
227 Self::Powershell => vec![],
229 Self::Cmd => vec![],
231 }
232 }
233
234 pub fn contains_path(path: &Path) -> bool {
236 let home_dir = home_dir();
237 std::env::var_os(EnvVars::PATH)
238 .as_ref()
239 .iter()
240 .flat_map(std::env::split_paths)
241 .map(|path| {
242 if let Some(home_dir) = home_dir.as_ref() {
244 if path
245 .components()
246 .next()
247 .map(std::path::Component::as_os_str)
248 == Some("~".as_ref())
249 {
250 return home_dir.join(path.components().skip(1).collect::<PathBuf>());
251 }
252 }
253 path
254 })
255 .any(|p| same_file::is_same_file(path, p).unwrap_or(false))
256 }
257
258 pub fn prepend_path(self, path: &Path) -> Option<String> {
260 match self {
261 Self::Nushell => None,
262 Self::Bash | Self::Zsh | Self::Ksh => Some(format!(
263 "export PATH=\"{}:$PATH\"",
264 backslash_escape(&path.simplified_display().to_string()),
265 )),
266 Self::Fish => Some(format!(
267 "fish_add_path \"{}\"",
268 backslash_escape(&path.simplified_display().to_string()),
269 )),
270 Self::Csh => Some(format!(
271 "setenv PATH \"{}:$PATH\"",
272 backslash_escape(&path.simplified_display().to_string()),
273 )),
274 Self::Powershell => Some(format!(
275 "$env:PATH = \"{};$env:PATH\"",
276 backtick_escape(&path.simplified_display().to_string()),
277 )),
278 Self::Cmd => Some(format!(
279 "set PATH=\"{};%PATH%\"",
280 backslash_escape(&path.simplified_display().to_string()),
281 )),
282 }
283 }
284}
285
286impl std::fmt::Display for Shell {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 Self::Bash => write!(f, "Bash"),
290 Self::Fish => write!(f, "Fish"),
291 Self::Powershell => write!(f, "PowerShell"),
292 Self::Cmd => write!(f, "Command Prompt"),
293 Self::Zsh => write!(f, "Zsh"),
294 Self::Nushell => write!(f, "Nushell"),
295 Self::Csh => write!(f, "Csh"),
296 Self::Ksh => write!(f, "Ksh"),
297 }
298 }
299}
300
301fn parse_shell_from_path(path: &Path) -> Option<Shell> {
303 let name = path.file_stem()?.to_str()?;
304 match name {
305 "bash" => Some(Shell::Bash),
306 "zsh" => Some(Shell::Zsh),
307 "fish" => Some(Shell::Fish),
308 "csh" => Some(Shell::Csh),
309 "ksh" => Some(Shell::Ksh),
310 "powershell" | "powershell_ise" => Some(Shell::Powershell),
311 _ => None,
312 }
313}
314
315fn backslash_escape(s: &str) -> String {
317 let mut escaped = String::with_capacity(s.len());
318 for c in s.chars() {
319 match c {
320 '\\' | '"' => escaped.push('\\'),
321 _ => {}
322 }
323 escaped.push(c);
324 }
325 escaped
326}
327
328fn backtick_escape(s: &str) -> String {
330 let mut escaped = String::with_capacity(s.len());
331 for c in s.chars() {
332 match c {
333 '"' | '`' | '\u{201C}' | '\u{201D}' | '\u{201E}' | '$' => escaped.push('`'),
336 _ => {}
337 }
338 escaped.push(c);
339 }
340 escaped
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use fs_err::File;
347 use temp_env::with_vars;
348 use tempfile::tempdir;
349
350 const HOME_DIR_ENV_VAR: &str = if cfg!(windows) {
352 EnvVars::USERPROFILE
353 } else {
354 EnvVars::HOME
355 };
356
357 #[test]
358 fn configuration_files_zsh_no_existing_zshenv() {
359 let tmp_home_dir = tempdir().unwrap();
360 let tmp_zdotdir = tempdir().unwrap();
361
362 with_vars(
363 [
364 (EnvVars::ZDOTDIR, None),
365 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
366 ],
367 || {
368 assert_eq!(
369 Shell::Zsh.configuration_files(),
370 vec![tmp_home_dir.path().join(".zshenv")]
371 );
372 },
373 );
374
375 with_vars(
376 [
377 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
378 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
379 ],
380 || {
381 assert_eq!(
382 Shell::Zsh.configuration_files(),
383 vec![tmp_zdotdir.path().join(".zshenv")]
384 );
385 },
386 );
387 }
388
389 #[test]
390 fn configuration_files_zsh_existing_home_zshenv() {
391 let tmp_home_dir = tempdir().unwrap();
392 File::create(tmp_home_dir.path().join(".zshenv")).unwrap();
393
394 let tmp_zdotdir = tempdir().unwrap();
395
396 with_vars(
397 [
398 (EnvVars::ZDOTDIR, None),
399 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
400 ],
401 || {
402 assert_eq!(
403 Shell::Zsh.configuration_files(),
404 vec![tmp_home_dir.path().join(".zshenv")]
405 );
406 },
407 );
408
409 with_vars(
410 [
411 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
412 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
413 ],
414 || {
415 assert_eq!(
416 Shell::Zsh.configuration_files(),
417 vec![tmp_home_dir.path().join(".zshenv")]
418 );
419 },
420 );
421 }
422
423 #[test]
424 fn configuration_files_zsh_existing_zdotdir_zshenv() {
425 let tmp_home_dir = tempdir().unwrap();
426
427 let tmp_zdotdir = tempdir().unwrap();
428 File::create(tmp_zdotdir.path().join(".zshenv")).unwrap();
429
430 with_vars(
431 [
432 (EnvVars::ZDOTDIR, tmp_zdotdir.path().to_str()),
433 (HOME_DIR_ENV_VAR, tmp_home_dir.path().to_str()),
434 ],
435 || {
436 assert_eq!(
437 Shell::Zsh.configuration_files(),
438 vec![tmp_zdotdir.path().join(".zshenv")]
439 );
440 },
441 );
442 }
443}