use crate::env;
use crate::hook_env;
use indoc::formatdoc;
use itertools::Itertools;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use usage_rs::spec::ValueEnum;
mod bash;
mod elvish;
mod fish;
mod nushell;
mod pwsh;
mod xonsh;
mod zsh;
#[derive(Debug, Clone, Copy, PartialEq, Eq, usage_rs::ValueEnum)]
pub(crate) enum ShellType {
Bash,
Elvish,
Fish,
Nu,
Xonsh,
Zsh,
#[usage(visible_alias = "powershell")]
Pwsh,
}
impl ShellType {
pub(crate) fn as_shell(&self) -> Box<dyn Shell> {
match self {
Self::Bash => Box::<bash::Bash>::default(),
Self::Elvish => Box::<elvish::Elvish>::default(),
Self::Fish => Box::<fish::Fish>::default(),
Self::Nu => Box::<nushell::Nushell>::default(),
Self::Xonsh => Box::<xonsh::Xonsh>::default(),
Self::Zsh => Box::<zsh::Zsh>::default(),
Self::Pwsh => Box::<pwsh::Pwsh>::default(),
}
}
}
impl Display for ShellType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bash => write!(f, "bash"),
Self::Elvish => write!(f, "elvish"),
Self::Fish => write!(f, "fish"),
Self::Nu => write!(f, "nu"),
Self::Xonsh => write!(f, "xonsh"),
Self::Zsh => write!(f, "zsh"),
Self::Pwsh => write!(f, "pwsh"),
}
}
}
impl FromStr for ShellType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
let s = s.rsplit_once(['/', '\\']).map(|(_, s)| s).unwrap_or(&s);
let s = s.strip_suffix(".exe").unwrap_or(s);
match s {
"bash" | "sh" => Ok(Self::Bash),
"elvish" => Ok(Self::Elvish),
"fish" => Ok(Self::Fish),
"nu" => Ok(Self::Nu),
"xonsh" => Ok(Self::Xonsh),
"zsh" => Ok(Self::Zsh),
"pwsh" | "powershell" => Ok(Self::Pwsh),
_ => Err(format!("unsupported shell type: {s}")),
}
}
}
pub(crate) trait Shell: Display {
fn activate(&self, opts: ActivateOptions) -> String;
fn deactivate(&self) -> String;
fn set_env(&self, k: &str, v: &str) -> String;
fn prepend_env(&self, k: &str, v: &str) -> String;
fn move_prepend_env(&self, k: &str, v: &str) -> String {
self.prepend_env(k, v)
}
fn supports_move_path(&self) -> bool {
false
}
fn unset_env(&self, k: &str) -> String;
fn set_alias(&self, name: &str, cmd: &str) -> String {
let _ = (name, cmd);
String::new()
}
fn unset_alias(&self, name: &str) -> String {
let _ = name;
String::new()
}
fn format_activate_prelude(&self, prelude: &[ActivatePrelude]) -> String {
prelude
.iter()
.map(|p| match p {
ActivatePrelude::Set(k, v) => self.set_env(k, v),
ActivatePrelude::Prepend(k, v) => self.prepend_env(k, v),
ActivatePrelude::MovePrepend(k, v) => self.move_prepend_env(k, v),
})
.join("")
}
}
pub(crate) enum ActivatePrelude {
Set(String, String),
Prepend(String, String),
MovePrepend(String, String),
}
pub(crate) struct ActivateOptions {
pub exe: PathBuf,
pub flags: String,
pub no_hook_env: bool,
pub prelude: Vec<ActivatePrelude>,
}
pub(crate) fn build_deactivation_script(shell: &dyn Shell) -> String {
if !env::is_activated() {
return String::new();
}
let mut out = hook_env::clear_old_env(shell);
out.push_str(&hook_env::clear_aliases(shell));
out.push_str(crate::packslip::completions::clear(&shell.to_string()));
out.push_str(&shell.deactivate());
out
}
pub(crate) fn get_shell(shell: Option<ShellType>) -> Option<Box<dyn Shell>> {
shell.or(*env::MISE_SHELL).map(|st| st.as_shell())
}
#[cfg(windows)]
pub(crate) const EXAMPLE_SHELL: &str = "pwsh";
#[cfg(not(windows))]
pub(crate) const EXAMPLE_SHELL: &str = "zsh";
pub(crate) fn require_shell(shell: Option<ShellType>, how: &str) -> eyre::Result<Box<dyn Shell>> {
get_shell(shell).ok_or_else(|| eyre::eyre!(no_shell_error(how)))
}
fn no_shell_error(how: &str) -> String {
let why = match cfg!(windows) {
true => {
"mise reads MISE_SHELL, then SHELL; neither names a shell it supports. PowerShell and cmd set neither.\n"
}
false => "",
};
let supported = ShellType::CHOICES.iter().join(", ");
formatdoc! {r#"
mise could not tell which shell to generate for.
{why}{how}
Supported shells: {supported}"#
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use usage_rs::spec::ValueEnum;
#[test]
fn a_windows_path_resolves_to_its_shell() {
for (input, expected) in [
(r"C:\Program Files\PowerShell\7\pwsh.exe", ShellType::Pwsh),
(r"C:\msys64\usr\bin\bash.exe", ShellType::Bash),
(r"C:\Program Files\Git\bin\PWSH.EXE", ShellType::Pwsh),
("pwsh.exe", ShellType::Pwsh),
] {
assert_eq!(
<ShellType as FromStr>::from_str(input),
Ok(expected),
"{input:?}"
);
}
}
#[test]
fn unix_paths_and_bare_names_are_unchanged() {
for (input, expected) in [
("/usr/bin/bash", ShellType::Bash),
("/bin/zsh", ShellType::Zsh),
("bash", ShellType::Bash),
("sh", ShellType::Bash),
("pwsh", ShellType::Pwsh),
("fish", ShellType::Fish),
] {
assert_eq!(
<ShellType as FromStr>::from_str(input),
Ok(expected),
"{input:?}"
);
}
}
#[test]
fn an_unsupported_shell_still_fails_but_names_itself() {
for input in [r"C:\WINDOWS\system32\cmd.exe", "cmd.exe"] {
assert_eq!(
<ShellType as FromStr>::from_str(input),
Err("unsupported shell type: cmd".to_string()),
"{input:?}"
);
}
}
#[test]
fn powershell_is_accepted_as_pwsh() {
assert_eq!(
<ShellType as FromStr>::from_str("powershell"),
Ok(ShellType::Pwsh),
"FromStr"
);
assert_eq!(
<ShellType as ValueEnum>::from_choice("powershell"),
Some(ShellType::Pwsh),
"usage-rs"
);
assert_eq!(
<ShellType as ValueEnum>::from_choice("pwsh"),
Some(ShellType::Pwsh)
);
}
#[test]
fn the_exe_suffix_is_stripped() {
for name in ["powershell.exe", "pwsh.exe", "PowerShell.exe"] {
assert_eq!(
<ShellType as FromStr>::from_str(name),
Ok(ShellType::Pwsh),
"{name}"
);
}
assert_eq!(
<ShellType as FromStr>::from_str("bash.exe"),
Ok(ShellType::Bash)
);
}
#[test]
fn the_primary_names_are_unchanged() {
let listed: Vec<&str> = ShellType::DETAILS
.iter()
.map(|choice| choice.value)
.collect();
assert_eq!(
listed,
["bash", "elvish", "fish", "nu", "xonsh", "zsh", "pwsh"]
);
}
#[test]
fn the_no_shell_message_says_what_to_do_and_what_is_accepted() {
let msg = no_shell_error("Name the shell: `mise activate zsh`.");
assert!(
msg.contains("Name the shell: `mise activate zsh`."),
"{msg}"
);
for shell in ShellType::CHOICES {
assert!(msg.contains(shell), "{shell} missing:\n{msg}");
}
}
#[test]
fn only_windows_explains_why_detection_failed() {
let msg = no_shell_error("Name the shell.");
assert_eq!(msg.contains("MISE_SHELL"), cfg!(windows), "{msg}");
assert_eq!(msg.contains("SHELL;"), cfg!(windows), "{msg}");
}
}