mod bash;
pub(crate) mod fish;
mod nushell;
mod posix;
mod powershell;
mod tcsh;
mod xonsh;
mod zsh;
use std::path::{Path, PathBuf};
use crate::config::Position;
use crate::context::SystemContext;
pub use crate::shell_kind::ShellKind;
pub trait Shell: Send + Sync {
fn kind(&self) -> ShellKind;
fn env_extension(&self) -> &'static str;
fn env_script(&self, dir: &Path, position: Position) -> String;
fn source_line(&self, env_script_path: &Path) -> String;
fn rc_candidates(&self, ctx: &SystemContext) -> Vec<PathBuf>;
fn primary_rc(&self, ctx: &SystemContext) -> PathBuf;
}
pub(crate) fn posix_env_script(dir: &Path, position: Position) -> String {
let dir = dir.display();
let path_expr = match position {
Position::Prepend => format!("\"{dir}:$PATH\""),
Position::Append => format!("\"$PATH:{dir}\""),
};
format!(
r#"#!/bin/sh
# Generated by onpath. Do not edit.
case ":${{PATH}}:" in
*:"{dir}":*)
;;
*)
export PATH={path_expr}
;;
esac
"#
)
}
pub(crate) fn posix_source_line(env_script_path: &Path) -> String {
format!(". \"{}\"", env_script_path.display())
}
pub fn all_shells() -> Vec<Box<dyn Shell>> {
vec![
Box::new(posix::Posix),
Box::new(bash::Bash),
Box::new(zsh::Zsh),
Box::new(fish::Fish),
Box::new(nushell::Nushell),
Box::new(powershell::PowerShell),
Box::new(tcsh::Tcsh),
Box::new(xonsh::Xonsh),
]
}
pub fn detect_shells(ctx: &SystemContext) -> Vec<Box<dyn Shell>> {
let user_shell = ctx.user_shell_name();
all_shells()
.into_iter()
.filter(|shell| {
if shell.kind() == ShellKind::Posix {
return cfg!(not(windows));
}
let has_rc_files = shell.rc_candidates(ctx).iter().any(|p| p.exists());
let is_user_shell = user_shell.is_some_and(|s| {
matches!(
(s, shell.kind()),
("bash", ShellKind::Bash)
| ("zsh", ShellKind::Zsh)
| ("fish", ShellKind::Fish)
| ("nu" | "nushell", ShellKind::Nushell)
| ("pwsh" | "powershell", ShellKind::PowerShell)
| ("tcsh" | "csh", ShellKind::Tcsh)
| ("xonsh", ShellKind::Xonsh)
)
});
has_rc_files || is_user_shell
})
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[cfg(not(windows))]
#[test]
fn detect_shells_always_includes_posix() {
let ctx = SystemContext::with_home(PathBuf::from("/nonexistent/home"));
let shells = detect_shells(&ctx);
let kinds: Vec<ShellKind> = shells.iter().map(|s| s.kind()).collect();
assert!(kinds.contains(&ShellKind::Posix));
}
#[test]
fn detect_shells_includes_user_shell() {
let mut env = HashMap::new();
env.insert("SHELL".to_owned(), "/usr/bin/fish".to_owned());
let ctx = SystemContext::with_home_and_env(PathBuf::from("/nonexistent/home"), env);
let shells = detect_shells(&ctx);
let kinds: Vec<ShellKind> = shells.iter().map(|s| s.kind()).collect();
assert!(kinds.contains(&ShellKind::Fish));
}
#[test]
fn detect_shells_includes_shell_with_existing_rc() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().to_owned();
std::fs::write(home.join(".bashrc"), "").unwrap();
let ctx = SystemContext::with_home(home);
let shells = detect_shells(&ctx);
let kinds: Vec<ShellKind> = shells.iter().map(|s| s.kind()).collect();
assert!(kinds.contains(&ShellKind::Bash));
}
#[cfg(not(windows))]
#[test]
fn detect_shells_empty_home_returns_only_posix() {
let ctx = SystemContext::with_home(PathBuf::from("/nonexistent/home"));
let shells = detect_shells(&ctx);
let kinds: Vec<ShellKind> = shells.iter().map(|s| s.kind()).collect();
assert_eq!(kinds, vec![ShellKind::Posix]);
}
#[test]
fn all_shells_returns_all_eight() {
assert_eq!(all_shells().len(), 8);
}
}