use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Mutex;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum Personality {
Zsh = 0,
Bash = 1,
Ksh93 = 2,
Mksh = 3,
Pdksh = 4,
Sh = 5,
Dash = 6,
Csh = 7,
}
impl Personality {
pub fn emulate_name(self) -> &'static str {
match self {
Personality::Zsh => "zsh",
Personality::Bash | Personality::Sh => "sh",
Personality::Ksh93 | Personality::Mksh | Personality::Pdksh => "ksh",
Personality::Dash => "dash",
Personality::Csh => "csh",
}
}
fn env_param(self) -> Option<&'static str> {
match self {
Personality::Bash => Some("BASH_ENV"),
Personality::Ksh93 | Personality::Mksh | Personality::Pdksh => Some("ENV"),
Personality::Sh | Personality::Dash => Some("ENV"),
Personality::Zsh | Personality::Csh => None,
}
}
fn default_env_file(self) -> Option<&'static str> {
match self {
Personality::Ksh93 => Some(".kshrc"),
Personality::Mksh => Some(".mkshrc"),
_ => None,
}
}
}
static PERSONALITY: AtomicU8 = AtomicU8::new(Personality::Zsh as u8);
static EMULATING: AtomicBool = AtomicBool::new(false);
static PERSONALITY_SET: AtomicBool = AtomicBool::new(false);
pub fn set_personality(p: Personality) {
PERSONALITY.store(p as u8, Ordering::Relaxed);
PERSONALITY_SET.store(true, Ordering::Relaxed);
}
#[inline]
pub fn set_emulating(on: bool) {
EMULATING.store(on, Ordering::Relaxed);
}
#[inline]
pub fn emulating() -> bool {
EMULATING.load(Ordering::Relaxed)
}
pub fn personality() -> Personality {
match PERSONALITY.load(Ordering::Relaxed) {
1 => Personality::Bash,
2 => Personality::Ksh93,
3 => Personality::Mksh,
4 => Personality::Pdksh,
5 => Personality::Sh,
6 => Personality::Dash,
7 => Personality::Csh,
_ => Personality::Zsh,
}
}
pub fn selected_emulate_name() -> Option<&'static str> {
PERSONALITY_SET
.load(Ordering::Relaxed)
.then(|| personality().emulate_name())
}
static NORC: AtomicBool = AtomicBool::new(false);
static NOPROFILE: AtomicBool = AtomicBool::new(false);
static RCFILE: Mutex<Option<String>> = Mutex::new(None);
static EXPLICIT_LOGIN: AtomicBool = AtomicBool::new(false);
const SYS_BASHRC: &str = "/etc/bash.bashrc";
const SYS_KSHRC: &str = "/etc/ksh.kshrc";
const SYS_PROFILE: &str = "/etc/profile";
const SYS_SUID_PROFILE: &str = "/etc/suid_profile";
const SYS_CSHRC: &str = "/etc/csh.cshrc";
const SYS_CSH_LOGIN: &str = "/etc/csh.login";
const SYS_CSH_LOGOUT: &str = "/etc/csh.logout";
const BASH_PROFILE_CHAIN: [&str; 3] = [".bash_profile", ".bash_login", ".profile"];
const CSH_RC_CHAIN: [&str; 2] = [".tcshrc", ".cshrc"];
#[inline]
pub fn set_explicit_login(on: bool) {
EXPLICIT_LOGIN.store(on, Ordering::Relaxed);
}
#[inline]
pub fn explicit_login() -> bool {
EXPLICIT_LOGIN.load(Ordering::Relaxed)
}
#[inline]
pub fn set_norc(on: bool) {
NORC.store(on, Ordering::Relaxed);
}
#[inline]
pub fn norc() -> bool {
NORC.load(Ordering::Relaxed)
}
#[inline]
pub fn set_noprofile(on: bool) {
NOPROFILE.store(on, Ordering::Relaxed);
}
#[inline]
pub fn noprofile() -> bool {
NOPROFILE.load(Ordering::Relaxed)
}
pub fn set_rcfile(path: &str) {
if let Ok(mut slot) = RCFILE.lock() {
*slot = Some(path.to_string());
}
}
pub fn rcfile() -> Option<String> {
RCFILE.lock().ok().and_then(|slot| slot.clone())
}
fn home() -> Option<PathBuf> {
crate::ported::params::getsparam("HOME").map(PathBuf::from)
}
fn sys_file(path: &str) -> Option<PathBuf> {
let p = PathBuf::from(path);
p.exists().then_some(p)
}
fn first_readable(home: Option<&Path>, chain: &[&str]) -> Option<PathBuf> {
let h = home?;
chain
.iter()
.map(|n| h.join(n))
.find(|p| std::fs::File::open(p).is_ok())
}
fn env_file(p: Personality, home: Option<&Path>) -> Option<PathBuf> {
let raw = p
.env_param()
.and_then(crate::ported::params::getsparam)
.filter(|v| !v.is_empty());
match raw {
Some(raw) => {
let expanded = if raw.contains('$') || raw.contains('`') || raw.starts_with('~') {
crate::ported::lex::untokenize(&crate::ported::subst::singsub(&raw))
} else {
raw
};
(!expanded.is_empty()).then(|| PathBuf::from(expanded))
}
None => p
.default_env_file()
.and_then(|name| home.map(|h| h.join(name))),
}
}
struct Ctx<'a> {
home: Option<&'a Path>,
env_file: Option<PathBuf>,
is_login: bool,
is_interactive: bool,
privileged: bool,
explicit_login: bool,
}
fn bourne_profile(c: &Ctx, out: &mut Vec<PathBuf>) {
if c.privileged {
out.extend(sys_file(SYS_SUID_PROFILE));
return;
}
out.push(PathBuf::from(SYS_PROFILE));
out.extend(c.home.map(|h| h.join(".profile")));
}
fn files_for(p: Personality, c: &Ctx) -> Vec<PathBuf> {
let mut files = Vec::new();
match p {
Personality::Zsh => {}
Personality::Bash => {
if c.privileged {
return files;
}
if c.is_login && (c.is_interactive || c.explicit_login) {
if !noprofile() {
files.push(PathBuf::from(SYS_PROFILE));
files.extend(first_readable(c.home, &BASH_PROFILE_CHAIN));
}
} else if !c.is_login && c.is_interactive && !norc() {
files.extend(sys_file(SYS_BASHRC));
match rcfile() {
Some(f) => files.push(PathBuf::from(f)),
None => files.extend(c.home.map(|h| h.join(".bashrc"))),
}
}
if !c.is_interactive {
files.extend(c.env_file.clone());
}
}
Personality::Ksh93
| Personality::Mksh
| Personality::Pdksh
| Personality::Sh
| Personality::Dash => {
if c.is_login {
bourne_profile(c, &mut files);
}
if c.is_interactive && !c.privileged {
if p == Personality::Ksh93 {
files.extend(sys_file(SYS_KSHRC));
}
files.extend(c.env_file.clone());
}
}
Personality::Csh => {
if c.privileged {
return files;
}
files.extend(sys_file(SYS_CSHRC));
files.extend(first_readable(c.home, &CSH_RC_CHAIN));
if c.is_login {
files.extend(sys_file(SYS_CSH_LOGIN));
files.extend(c.home.map(|h| h.join(".login")));
}
}
}
files
}
pub fn startup_files(is_login: bool, is_interactive: bool, privileged: bool) -> Vec<PathBuf> {
let p = personality();
let home = home();
files_for(
p,
&Ctx {
home: home.as_deref(),
env_file: env_file(p, home.as_deref()),
is_login,
is_interactive,
privileged,
explicit_login: explicit_login(),
},
)
}
pub fn logout_files(is_interactive: bool) -> Vec<PathBuf> {
let home = home();
match personality() {
Personality::Bash => home.map(|h| h.join(".bash_logout")).into_iter().collect(),
Personality::Csh if is_interactive => home
.map(|h| h.join(".logout"))
.into_iter()
.chain(sys_file(SYS_CSH_LOGOUT))
.collect(),
_ => Vec::new(),
}
}
pub fn apply_personality_option_deltas() {
use crate::ported::options::opt_state_set;
let p = personality();
if p == Personality::Zsh {
return;
}
opt_state_set("promptsp", false);
if p != Personality::Bash {
return;
}
opt_state_set("ignorebraces", false);
opt_state_set("bashrematch", true);
opt_state_set("promptpercent", true);
opt_state_set("promptsubst", true);
crate::dash_mode::set_bash_mode(true);
crate::dash_mode::bash_shopt_apply_defaults();
}
pub fn emits_bracketed_paste() -> bool {
matches!(personality(), Personality::Zsh | Personality::Bash)
}
pub fn emits_integration_prompt() -> bool {
personality() == Personality::Zsh
}
const KSH93_ALIASES: &[(&str, &str)] = &[];
const MKSH_ALIASES: &[(&str, &str)] = &[
("autoload", r"\builtin typeset -fu"),
("functions", r"\builtin typeset -f"),
("hash", r"\builtin alias -t"),
("history", r"\builtin fc -l"),
("integer", r"\builtin typeset -i"),
("local", r"\builtin typeset"),
("login", r"\builtin exec login"),
("nameref", r"\builtin typeset -n"),
("nohup", "nohup "),
("r", r"\builtin fc -e -"),
("type", r"\builtin whence -v"),
];
pub fn default_aliases() -> Option<&'static [(&'static str, &'static str)]> {
match personality() {
Personality::Zsh => None,
Personality::Ksh93 => Some(KSH93_ALIASES),
Personality::Mksh | Personality::Pdksh => Some(MKSH_ALIASES),
_ => Some(&[]),
}
}
pub fn install_default_aliases() {
let Some(defaults) = default_aliases() else {
return;
};
let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() else {
return;
};
tab.clear();
for (name, text) in defaults {
tab.add(crate::ported::hashtable::createaliasnode(name, text, 0));
}
}
pub fn single_quote(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('\'');
for c in value.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
pub fn overrides_zsh_startup() -> bool {
personality() != Personality::Zsh
}
pub fn run_init_scripts() {
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::RCS) {
return;
}
let files = startup_files(
crate::ported::zsh_h::islogin(),
crate::ported::zsh_h::interact(),
crate::ported::zsh_h::isset(crate::ported::zsh_h::PRIVILEGED),
);
for f in files {
let _ = crate::ported::init::source(&f.to_string_lossy());
}
}
pub fn run_logout_scripts() {
if !crate::ported::zsh_h::islogin() {
return;
}
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::RCS) {
return;
}
for f in logout_files(crate::ported::zsh_h::interact()) {
let _ = crate::ported::init::source(&f.to_string_lossy());
}
}
#[cfg(test)]
mod tests {
use super::*;
static GUARD: Mutex<()> = Mutex::new(());
fn reset() {
set_norc(false);
set_noprofile(false);
if let Ok(mut slot) = RCFILE.lock() {
*slot = None;
}
}
fn fake_home(files: &[&str]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
for f in files {
std::fs::write(dir.path().join(f), "").expect("write");
}
dir
}
fn ctx<'a>(home: &'a Path, env: Option<&str>, login: bool, interactive: bool) -> Ctx<'a> {
Ctx {
home: Some(home),
env_file: env.map(PathBuf::from),
is_login: login,
is_interactive: interactive,
privileged: false,
explicit_login: true,
}
}
fn names(files: Vec<PathBuf>) -> Vec<String> {
files
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
}
fn tails(files: Vec<PathBuf>) -> Vec<String> {
files
.into_iter()
.filter(|p| !p.starts_with("/etc"))
.map(|p| {
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
})
.collect()
}
#[test]
fn bash_login_reads_profile_not_bashrc() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bash_profile", ".bash_login", ".profile", ".bashrc"]);
let files = files_for(Personality::Bash, &ctx(home.path(), None, true, true));
assert!(
names(files.clone()).contains(&SYS_PROFILE.to_string()),
"a login bash reads /etc/profile first"
);
assert_eq!(tails(files), vec![".bash_profile".to_string()]);
}
#[test]
fn bash_profile_chain_falls_through_in_order() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
for (present, expected) in [
(vec![".bash_login", ".profile"], ".bash_login"),
(vec![".profile"], ".profile"),
] {
let home = fake_home(&present);
assert_eq!(
tails(files_for(
Personality::Bash,
&ctx(home.path(), None, true, true)
))
.last()
.map(String::as_str),
Some(expected),
"with {present:?} present, bash reads {expected}"
);
}
let home = fake_home(&[]);
assert_eq!(
names(files_for(
Personality::Bash,
&ctx(home.path(), None, true, true)
)),
vec![SYS_PROFILE.to_string()],
"an empty home leaves a login shell with /etc/profile alone"
);
}
#[test]
fn bash_interactive_nonlogin_reads_bashrc_only() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bashrc", ".bash_profile", ".zshrc"]);
let files = tails(files_for(
Personality::Bash,
&ctx(home.path(), None, false, true),
));
assert!(files.contains(&".bashrc".to_string()), "got {files:?}");
assert!(!files.iter().any(|f| f == ".bash_profile"), "got {files:?}");
assert!(!files.iter().any(|f| f.contains("zsh")), "got {files:?}");
}
#[test]
fn bash_norc_and_noprofile_suppress_their_phase() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bashrc", ".bash_profile"]);
set_norc(true);
assert!(files_for(Personality::Bash, &ctx(home.path(), None, false, true)).is_empty());
reset();
set_noprofile(true);
assert!(files_for(Personality::Bash, &ctx(home.path(), None, true, true)).is_empty());
reset();
}
#[test]
fn bash_rcfile_overrides_bashrc() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bashrc"]);
set_rcfile("/tmp/zshrs-alt-bashrc");
let files = names(files_for(
Personality::Bash,
&ctx(home.path(), None, false, true),
));
assert!(
files.iter().any(|f| f == "/tmp/zshrs-alt-bashrc"),
"got {files:?}"
);
assert!(
!files.iter().any(|f| f.ends_with("/.bashrc")),
"got {files:?}"
);
reset();
}
#[test]
fn bash_env_is_non_interactive_only_and_last() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bashrc", ".bash_profile"]);
let probe = "/tmp/zshrs-env-probe";
assert_eq!(
tails(files_for(
Personality::Bash,
&ctx(home.path(), Some(probe), true, false)
)),
vec![".bash_profile".to_string(), "zshrs-env-probe".to_string()],
"profile chain first, $BASH_ENV last"
);
assert_eq!(
names(files_for(
Personality::Bash,
&ctx(home.path(), Some(probe), false, false)
)),
vec![probe.to_string()],
);
assert!(
!names(files_for(
Personality::Bash,
&ctx(home.path(), Some(probe), false, true)
))
.iter()
.any(|f| f == probe),
"an interactive bash never reads $BASH_ENV"
);
}
#[test]
fn korn_and_bourne_interactive_login_read_both() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".profile"]);
let probe = "/tmp/zshrs-env-probe";
for p in [
Personality::Ksh93,
Personality::Mksh,
Personality::Pdksh,
Personality::Sh,
Personality::Dash,
] {
let files = files_for(p, &ctx(home.path(), Some(probe), true, true));
assert!(
names(files.clone()).contains(&SYS_PROFILE.to_string()),
"{p:?} interactive login reads /etc/profile"
);
assert_eq!(
tails(files),
vec![".profile".to_string(), "zshrs-env-probe".to_string()],
"{p:?} interactive login reads ~/.profile, then $ENV"
);
}
}
#[test]
fn korn_and_bourne_non_interactive_read_no_rc() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".profile", ".kshrc", ".mkshrc"]);
let probe = "/tmp/zshrs-env-probe";
for p in [Personality::Ksh93, Personality::Mksh, Personality::Dash] {
assert!(
files_for(p, &ctx(home.path(), Some(probe), false, false)).is_empty(),
"{p:?} non-interactive non-login reads nothing"
);
}
}
#[test]
fn env_defaults_match_each_korn_line() {
let home = fake_home(&[]);
assert_eq!(
env_file(Personality::Ksh93, Some(home.path())),
Some(home.path().join(".kshrc"))
);
assert_eq!(
env_file(Personality::Mksh, Some(home.path())),
Some(home.path().join(".mkshrc"))
);
for p in [Personality::Pdksh, Personality::Sh, Personality::Dash] {
assert_eq!(
env_file(p, Some(home.path())),
None,
"{p:?} documents no default for $ENV"
);
}
}
#[test]
fn csh_reads_cshrc_always_and_login_after() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".tcshrc", ".cshrc", ".login"]);
assert_eq!(
tails(files_for(
Personality::Csh,
&ctx(home.path(), None, false, true)
)),
vec![".tcshrc".to_string()],
);
assert_eq!(
tails(files_for(
Personality::Csh,
&ctx(home.path(), None, true, true)
)),
vec![".tcshrc".to_string(), ".login".to_string()],
);
let no_tcshrc = fake_home(&[".cshrc"]);
assert_eq!(
tails(files_for(
Personality::Csh,
&ctx(no_tcshrc.path(), None, false, true)
)),
vec![".cshrc".to_string()],
"~/.cshrc is the fallback when ~/.tcshrc is absent"
);
}
#[test]
fn privileged_shell_reads_no_user_file() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bashrc", ".bash_profile", ".profile", ".kshrc", ".cshrc"]);
for p in [
Personality::Bash,
Personality::Ksh93,
Personality::Mksh,
Personality::Pdksh,
Personality::Sh,
Personality::Dash,
Personality::Csh,
] {
for (login, interactive) in [(true, true), (true, false), (false, true), (false, false)]
{
let files = names(files_for(
p,
&Ctx {
home: Some(home.path()),
env_file: Some(PathBuf::from("/tmp/zshrs-env-probe")),
is_login: login,
is_interactive: interactive,
privileged: true,
explicit_login: true,
},
));
assert!(
files.iter().all(|f| f.starts_with("/etc/")),
"{p:?} privileged (login={login}, interactive={interactive}) read {files:?}"
);
}
}
}
#[test]
fn zsh_personality_contributes_nothing() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".zshrc", ".profile", ".bashrc"]);
assert!(files_for(
Personality::Zsh,
&ctx(home.path(), Some("/tmp/zshrs-env-probe"), true, true)
)
.is_empty());
assert_eq!(Personality::Zsh.emulate_name(), "zsh");
}
#[test]
fn emulate_names_match_the_cli_modes() {
assert_eq!(Personality::Bash.emulate_name(), "sh");
assert_eq!(Personality::Sh.emulate_name(), "sh");
assert_eq!(Personality::Ksh93.emulate_name(), "ksh");
assert_eq!(Personality::Mksh.emulate_name(), "ksh");
assert_eq!(Personality::Pdksh.emulate_name(), "ksh");
assert_eq!(Personality::Dash.emulate_name(), "dash");
assert_eq!(Personality::Csh.emulate_name(), "csh");
}
#[test]
fn implicit_login_reads_no_bash_profile_but_does_for_the_others() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
reset();
let home = fake_home(&[".bash_profile", ".bashrc", ".profile", ".kshrc"]);
let implicit = |p: Personality, interactive: bool| {
tails(files_for(
p,
&Ctx {
home: Some(home.path()),
env_file: None,
is_login: true,
is_interactive: interactive,
privileged: false,
explicit_login: false,
},
))
};
assert!(
implicit(Personality::Bash, false).is_empty(),
"`-bash -c CMD` reads no startup file, got {:?}",
implicit(Personality::Bash, false)
);
assert_eq!(
implicit(Personality::Bash, true),
vec![".bash_profile".to_string()],
);
for p in [Personality::Ksh93, Personality::Sh, Personality::Dash] {
assert!(
implicit(p, false).contains(&".profile".to_string()),
"{p:?} reads ~/.profile for a non-interactive implicit login shell"
);
}
}
#[test]
fn personality_round_trips_through_the_atomic() {
let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
let saved = personality();
for p in [
Personality::Zsh,
Personality::Bash,
Personality::Ksh93,
Personality::Mksh,
Personality::Pdksh,
Personality::Sh,
Personality::Dash,
Personality::Csh,
] {
set_personality(p);
assert_eq!(personality(), p);
assert_eq!(selected_emulate_name(), Some(p.emulate_name()));
}
set_personality(saved);
}
}