///|
/// The shells shrc knows how to write to, and where their startup files live.
///
/// shrc treats each shell as a *family* (POSIX, fish, csh, nushell) plus a
/// startup file. Environment variables and aliases mean the same thing in
/// every shell; only the syntax differs, so the entry list is shared and the
/// rendering is not.
///|
/// The syntax family a startup file is written in.
pub(all) enum ShellFamily {
Posix
Fish
Csh
Nu
PowerShell
}
///|
/// One startup file: the shell it belongs to, its syntax family and its path.
pub(all) struct Shell {
name : String
family : ShellFamily
rc : String
}
///|
/// Build a shell record.
fn shell(name : String, family : ShellFamily, rc : String) -> Shell {
{ name, family, rc, }
}
///|
/// Every startup file shrc knows about, given the relevant base directories.
/// Pure, so tests can pin the layout.
pub fn candidate_shells(
home : String,
xdg : String,
zdotdir : String,
) -> Array[Shell] {
[
shell("profile", Posix, path_join(home, ".profile")),
shell("bash", Posix, path_join(home, ".bashrc")),
shell("bash_profile", Posix, path_join(home, ".bash_profile")),
shell("bash_login", Posix, path_join(home, ".bash_login")),
shell("zsh", Posix, path_join(zdotdir, ".zshrc")),
shell("zprofile", Posix, path_join(zdotdir, ".zprofile")),
shell("zshenv", Posix, path_join(zdotdir, ".zshenv")),
shell("ksh", Posix, path_join(home, ".kshrc")),
shell("mksh", Posix, path_join(home, ".mkshrc")),
shell("fish", Fish, path_join(xdg, "fish/config.fish")),
shell("csh", Csh, path_join(home, ".cshrc")),
shell("tcsh", Csh, path_join(home, ".tcshrc")),
shell("csh_login", Csh, path_join(home, ".login")),
shell("nu", Nu, path_join(xdg, "nushell/config.nu")),
shell("nu_env", Nu, path_join(xdg, "nushell/env.nu")),
// PowerShell 7 on Linux and macOS reads its profile from the config dir.
shell(
"pwsh",
PowerShell,
path_join(xdg, "powershell/Microsoft.PowerShell_profile.ps1"),
),
// PowerShell 7 and Windows PowerShell 5.1 on Windows keep theirs under
// Documents; `home` is %USERPROFILE% there.
shell(
"powershell",
PowerShell,
path_join(home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
),
shell(
"windows_powershell",
PowerShell,
path_join(
home, "Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1",
),
),
]
}
///|
/// The first path that exists, or the first one as the file to create.
fn first_existing(paths : Array[String], exists : (String) -> Bool) -> String {
for path in paths {
if exists(path) {
return path
}
}
paths[0]
}
///|
/// Build the startup file for a login shell, preferring a file that already
/// exists so a login file is never shadowed by a new interactive one.
fn pick_shell(
name : String,
family : ShellFamily,
paths : Array[String],
exists : (String) -> Bool,
) -> Shell {
shell(name, family, first_existing(paths, exists))
}
///|
/// The startup file for the login shell, or `None` for a shell shrc does not
/// know.
fn login_primary(
login : String,
home : String,
xdg : String,
zdotdir : String,
exists : (String) -> Bool,
) -> Shell? {
match login {
"bash" =>
Some(
pick_shell(
"bash",
Posix,
[
path_join(home, ".bashrc"),
path_join(home, ".bash_profile"),
path_join(home, ".bash_login"),
],
exists,
),
)
"zsh" =>
Some(
pick_shell(
"zsh",
Posix,
[
path_join(zdotdir, ".zshrc"),
path_join(zdotdir, ".zprofile"),
path_join(zdotdir, ".zshenv"),
],
exists,
),
)
"ksh" =>
Some(
pick_shell(
"ksh",
Posix,
[path_join(home, ".kshrc"), path_join(home, ".profile")],
exists,
),
)
"mksh" =>
Some(
pick_shell(
"mksh",
Posix,
[path_join(home, ".mkshrc"), path_join(home, ".profile")],
exists,
),
)
"sh" | "dash" | "ash" =>
Some(pick_shell("profile", Posix, [path_join(home, ".profile")], exists))
"fish" =>
Some(
pick_shell("fish", Fish, [path_join(xdg, "fish/config.fish")], exists),
)
"csh" =>
Some(
pick_shell(
"csh",
Csh,
[path_join(home, ".cshrc"), path_join(home, ".login")],
exists,
),
)
"tcsh" =>
Some(
pick_shell(
"tcsh",
Csh,
[
path_join(home, ".tcshrc"),
path_join(home, ".cshrc"),
path_join(home, ".login"),
],
exists,
),
)
"nu" =>
Some(
pick_shell(
"nu",
Nu,
[
path_join(xdg, "nushell/config.nu"),
path_join(xdg, "nushell/env.nu"),
],
exists,
),
)
"pwsh" | "powershell" | "pwsh.exe" | "powershell.exe" =>
Some(
pick_shell(
"pwsh",
PowerShell,
[
path_join(xdg, "powershell/Microsoft.PowerShell_profile.ps1"),
path_join(
home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1",
),
path_join(
home, "Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1",
),
],
exists,
),
)
_ => None
}
}
///|
/// Whether `rc` is already represented in `shells`.
fn has_rc(shells : Array[Shell], rc : String) -> Bool {
for entry in shells {
if entry.rc == rc {
return true
}
}
false
}
///|
/// Detect the startup files shrc should manage.
///
/// A file is managed when it already exists, so every shell the user has
/// configured is covered. The login shell's startup file is always included,
/// creating it when the shell has never been configured, and `~/.profile` is
/// the portable fallback when nothing else is found. `exists` is injected so
/// the decision is testable.
pub fn detect_shells(
home : String,
xdg : String,
zdotdir : String,
login_shell : String?,
exists : (String) -> Bool,
) -> Array[Shell] {
let login = match login_shell {
Some(s) => path_basename(s)
None => ""
}
let out = []
for candidate in candidate_shells(home, xdg, zdotdir) {
if exists(candidate.rc) && !has_rc(out, candidate.rc) {
out.push(candidate)
}
}
match login_primary(login, home, xdg, zdotdir, exists) {
Some(primary) => if !has_rc(out, primary.rc) { out.push(primary) }
None => ()
}
if out.length() == 0 {
out.push(shell("profile", Posix, path_join(home, ".profile")))
}
out
}
///|
/// Detect shells against the real filesystem and `$SHELL`.
pub fn current_shells() -> Array[Shell] {
detect_shells(
home_dir(),
xdg_config_home(),
zdotdir(),
@env.get_env_var("SHELL"),
@fs.path_exists,
)
}
///|
/// Every startup file shrc knows about, existing or not. Used to describe the
/// full surface shrc can manage.
pub fn all_candidate_shells() -> Array[Shell] {
candidate_shells(home_dir(), xdg_config_home(), zdotdir())
}
///|
/// The syntax family to use for the shell that will evaluate shrc's output.
/// `$SHELL` names the login shell; anything unknown is treated as POSIX.
pub fn eval_family(login_shell : String?) -> ShellFamily {
match login_shell {
Some(s) =>
match path_basename(s) {
"fish" => Fish
"csh" | "tcsh" => Csh
"nu" => Nu
"pwsh" | "powershell" | "pwsh.exe" | "powershell.exe" => PowerShell
_ => Posix
}
None => Posix
}
}
///|
/// The family of the shell shrc is running under. `$SHELL` is authoritative
/// where it is set (Unix, Git Bash); otherwise `PSModulePath` identifies
/// PowerShell, and anything else is treated as POSIX.
pub fn current_eval_family() -> ShellFamily {
match @env.get_env_var("SHELL") {
Some(s) => eval_family(Some(s))
None =>
if @env.get_env_var("PSModulePath") is Some(_) {
PowerShell
} else {
Posix
}
}
}