pub(crate) use std::path::*;
use crate::dirs;
pub(crate) fn settle_display_separators(s: String) -> String {
match cfg!(windows) {
true => s.replace('/', "\\"),
false => s,
}
}
#[cfg(windows)]
fn simplify_verbatim_unc(shown: String) -> String {
const VERBATIM_UNC: &str = r"\\?\UNC\";
let Some(rest) = shown.strip_prefix(VERBATIM_UNC) else {
return shown;
};
let plain_len = 2 + rest.encode_utf16().count();
if rest.contains('/')
|| plain_len >= crate::file::MAX_PATH
|| rest
.split('\\')
.any(|c| c.ends_with('.') || c.ends_with(' '))
{
return shown;
}
format!(r"\\{rest}")
}
#[cfg(not(windows))]
fn simplify_verbatim_unc(shown: String) -> String {
shown
}
pub(crate) trait PathExt {
fn display_user(&self) -> String;
fn mount(&self, on: &Path) -> PathBuf;
fn is_empty(&self) -> bool;
}
impl PathExt for Path {
fn display_user(&self) -> String {
let path = dunce::simplified(self);
let home = dirs::HOME.to_string_lossy();
let home_str: &str = home.as_ref();
let shown = match cfg!(unix) && path.starts_with(home_str) && home != "/" {
true => path.to_string_lossy().replacen(home_str, "~", 1),
false => path.to_string_lossy().to_string(),
};
simplify_verbatim_unc(shown)
}
fn mount(&self, on: &Path) -> PathBuf {
if PathExt::is_empty(self) {
on.to_path_buf()
} else {
on.join(self)
}
}
fn is_empty(&self) -> bool {
self.as_os_str().is_empty()
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn program_stem(program: &Path) -> Option<String> {
let s = program.to_str()?;
let basename = s.rsplit(['/', '\\']).next().unwrap_or(s);
let stem = match basename.rsplit_once('.') {
Some((stem, ext)) if ext.eq_ignore_ascii_case("exe") => stem,
_ => basename,
};
Some(stem.to_ascii_lowercase())
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn is_posix_shell_program(program: &Path) -> bool {
const POSIX_SHELLS: &[&str] = &["bash", "sh", "zsh", "fish", "ksh", "dash", "ash"];
let Some(stem) = program_stem(program) else {
return false;
};
POSIX_SHELLS.iter().any(|name| *name == stem)
}
pub(crate) fn command_mode_script_payload(program: &Path) -> Option<&'static str> {
if program_stem(program).as_deref() == Some("fish") {
return Some("$argv");
}
is_posix_shell_program(program).then_some(r#""$0" "$@""#)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn is_cmd_shell_program(program: &Path) -> bool {
program_stem(program).as_deref() == Some("cmd")
}
pub(crate) fn is_powershell_program(program: &Path) -> bool {
matches!(
program_stem(program).as_deref(),
Some("pwsh" | "powershell")
)
}
pub(crate) fn inject_powershell_no_profile(shell: &mut Vec<String>) {
let Some(program) = shell.first() else {
return;
};
if !is_powershell_program(Path::new(program)) {
return;
}
let already_present = shell[1..]
.iter()
.take_while(|arg| {
let token = arg.trim_start_matches(['-', '/']).to_ascii_lowercase();
!(!token.is_empty()
&& ("command".starts_with(&token)
|| "commandwithargs".starts_with(&token)
|| "file".starts_with(&token)))
})
.any(|arg| {
let token = arg.trim_start_matches(['-', '/']).to_ascii_lowercase();
token.len() >= 3 && "noprofile".starts_with(&token)
});
if !already_present {
shell.insert(1, "-NoProfile".to_string());
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn cmd_verbatim_args(
shell_flags: &[String],
script: &str,
args: &[String],
) -> Vec<String> {
let mut body = script.to_string();
for arg in args {
body.push(' ');
body.push_str("e_arg_for_cmd_body(arg));
}
let mut out: Vec<String> = Vec::with_capacity(shell_flags.len() + 2);
if !shell_flags.iter().any(|f| f.eq_ignore_ascii_case("/s")) {
out.push("/s".to_string());
}
out.extend(shell_flags.iter().cloned());
out.push(format!("\"{body}\""));
out
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn quote_arg_for_cmd_body(arg: &str) -> String {
if !arg.is_empty() && !arg.contains([' ', '\t', '"', '&', '|', '<', '>', '(', ')', '^']) {
return arg.to_string();
}
let mut s = String::with_capacity(arg.len() + 2);
s.push('"');
let mut backslashes = 0usize;
for c in arg.chars() {
if c == '\\' {
backslashes += 1;
} else {
if c == '"' {
for _ in 0..=backslashes {
s.push('\\');
}
}
backslashes = 0;
}
s.push(c);
}
for _ in 0..backslashes {
s.push('\\');
}
s.push('"');
s
}
#[cfg(windows)]
pub(crate) fn cmd_verbatim_command(
program: &str,
flags: &[String],
body: &str,
) -> Option<std::process::Command> {
use std::os::windows::process::CommandExt;
let runs_command = flags
.iter()
.any(|f| f.eq_ignore_ascii_case("/c") || f.eq_ignore_ascii_case("/k"));
if !is_cmd_shell_program(Path::new(program)) || !runs_command {
return None;
}
let mut c = std::process::Command::new(program);
for a in cmd_verbatim_args(flags, body, &[]) {
c.raw_arg(a);
}
Some(c)
}
pub(crate) fn split_shell_command(s: &str) -> eyre::Result<Vec<String>> {
#[cfg(windows)]
{
split_shell_command_windows(s)
}
#[cfg(not(windows))]
{
Ok(shell_words::split(s)?)
}
}
#[cfg(windows)]
fn split_shell_command_windows(s: &str) -> eyre::Result<Vec<String>> {
let mut args: Vec<String> = Vec::new();
let mut cur = String::new();
let mut in_token = false;
let mut in_quotes = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '"' {
in_token = true;
if in_quotes {
if chars.peek() == Some(&'"') {
cur.push('"');
chars.next();
} else {
in_quotes = false;
}
} else {
in_quotes = true;
}
} else if c.is_whitespace() && !in_quotes {
if in_token {
args.push(std::mem::take(&mut cur));
in_token = false;
}
} else {
in_token = true;
cur.push(c);
}
}
if in_quotes {
return Err(eyre::eyre!("unbalanced quote in shell command: {s}"));
}
if in_token {
args.push(cur);
}
Ok(args)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn unix_path_to_windows(entry: &str) -> Option<String> {
if let Some(rest) = entry.strip_prefix("//")
&& !rest.is_empty()
&& !rest.starts_with('/')
{
return Some(format!(r"\\{}", rest.replace('/', r"\")));
}
let bytes = entry.as_bytes();
if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
return Some(entry.replace('/', r"\"));
}
let unprefixed = entry.strip_prefix("/cygdrive").unwrap_or(entry);
let b = unprefixed.as_bytes();
if b.len() >= 2 && b[0] == b'/' && b[1].is_ascii_alphabetic() && (b.len() == 2 || b[2] == b'/')
{
let drive = (b[1] as char).to_ascii_uppercase();
let tail = unprefixed[2..].replace('/', r"\"); return Some(if tail == r"\" || tail.is_empty() {
format!(r"{drive}:\")
} else {
format!("{drive}:{tail}")
});
}
None
}
#[cfg(windows)]
pub(crate) fn resolve_posix_shell_program_path(
program: &std::ffi::OsStr,
env: &std::collections::BTreeMap<String, String>,
) -> Option<std::ffi::OsString> {
if !is_posix_shell_program(Path::new(program)) {
return None;
}
if program_has_directory_component(program) {
return None;
}
let is_bash = is_bash_basename(program);
if is_bash {
let override_path = env
.get("MISE_BASH_PATH")
.cloned()
.or_else(|| std::env::var("MISE_BASH_PATH").ok())
.filter(|s| !s.is_empty());
if let Some(p) = override_path {
let path = PathBuf::from(&p);
if path.is_file() {
return Some(path.into_os_string());
}
warn!("MISE_BASH_PATH={p} does not exist; falling back to other candidates");
}
}
let path_val = env.get(&*crate::env::PATH_KEY)?;
if !path_val.contains(';') && !path_val.contains('\\') {
return None;
}
if is_bash {
for candidate in bash_candidates(env) {
if candidate.is_file() {
return Some(candidate.into_os_string());
}
}
}
let cwd = std::env::current_dir().ok()?;
if is_bash {
let mut all = which::which_in_all(program, Some(path_val.as_str()), cwd).ok()?;
if let Some(p) = all.find(|p| !is_wsl_launcher_bash(p)) {
return Some(p.into_os_string());
}
warn!(
"no real POSIX bash found on PATH (only the WSL launcher) when resolving bash; \
install Git Bash or MSYS2, or set MISE_BASH_PATH to a real POSIX bash to silence this"
);
return None;
}
which::which_in(program, Some(path_val.as_str()), cwd)
.ok()
.map(|p| p.into_os_string())
}
#[cfg(windows)]
fn is_bash_basename(program: &std::ffi::OsStr) -> bool {
program_stem(Path::new(program)).as_deref() == Some("bash")
}
#[cfg(windows)]
fn program_has_directory_component(program: &std::ffi::OsStr) -> bool {
Path::new(program).components().count() > 1
}
#[cfg(windows)]
fn bash_candidates(env: &std::collections::BTreeMap<String, String>) -> Vec<PathBuf> {
let mut candidates = vec![
PathBuf::from(r"C:\Program Files\Git\bin\bash.exe"),
PathBuf::from(r"C:\Program Files (x86)\Git\bin\bash.exe"),
];
let local_appdata = env
.get("LOCALAPPDATA")
.cloned()
.or_else(|| std::env::var("LOCALAPPDATA").ok());
if let Some(local) = local_appdata.filter(|s| !s.is_empty()) {
candidates.push(PathBuf::from(local).join(r"Programs\Git\bin\bash.exe"));
}
candidates.push(PathBuf::from(r"C:\msys64\usr\bin\bash.exe"));
candidates.push(PathBuf::from(r"C:\msys32\usr\bin\bash.exe"));
candidates
}
#[cfg(windows)]
pub(crate) fn is_wsl_launcher_bash(path: &Path) -> bool {
let Some(s) = path.to_str() else {
return false;
};
let lower = s.to_ascii_lowercase().replace('/', "\\");
lower.ends_with(r"\windows\system32\bash.exe")
|| lower.contains(r"\microsoft\windowsapps\bash.exe")
}
#[cfg(test)]
mod tests {
use super::*;
fn sv(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[cfg(windows)]
fn env_with_path(path: &str) -> std::collections::BTreeMap<String, String> {
let mut env = std::collections::BTreeMap::new();
env.insert((*crate::env::PATH_KEY).to_string(), path.to_string());
env.insert("OTHER".to_string(), "unchanged".to_string());
env
}
#[cfg(windows)]
#[test]
fn test_display_user_drops_the_extended_length_prefix() {
assert_eq!(
Path::new(r"\\?\C:\Users\me\proj").display_user(),
r"C:\Users\me\proj"
);
assert_eq!(
Path::new(r"C:\Users\me\proj").display_user(),
r"C:\Users\me\proj"
);
assert_eq!(
Path::new(r"\\server\share\proj").display_user(),
r"\\server\share\proj"
);
assert_eq!(
Path::new(r"\\?\UNC\server\share").display_user(),
r"\\server\share"
);
assert_eq!(
Path::new(r"\\?\UNC\wsl.localhost\Ubuntu\home\me\proj").display_user(),
r"\\wsl.localhost\Ubuntu\home\me\proj"
);
assert_eq!(Path::new(r"\\.\COM1").display_user(), r"\\.\COM1");
assert_eq!(
Path::new(r"\\?\C:\proj\CON").display_user(),
r"\\?\C:\proj\CON"
);
}
#[cfg(windows)]
#[test]
fn test_display_user_keeps_the_prefix_when_unc_needs_it() {
assert_eq!(
Path::new(r"\\?\UNC\server\share\a/b").display_user(),
r"\\?\UNC\server\share\a/b"
);
let long = format!(r"\\?\UNC\server\share\{}", "d".repeat(260));
assert_eq!(Path::new(&long).display_user(), long);
assert_eq!(
Path::new(r"\\?\UNC\server\share\proj.").display_user(),
r"\\?\UNC\server\share\proj."
);
assert_eq!(
Path::new(r"\\?\UNC\server\share\proj ").display_user(),
r"\\?\UNC\server\share\proj "
);
}
#[test]
fn test_settle_display_separators() {
#[cfg(windows)]
{
assert_eq!(
settle_display_separators(r"C:/Users/me\proj\.git/hooks".to_string()),
r"C:\Users\me\proj\.git\hooks"
);
assert_eq!(
Path::new("C:/Users/me").display_user(),
"C:/Users/me",
"display_user must not settle separators"
);
}
#[cfg(not(windows))]
{
assert_eq!(settle_display_separators(r"/a/b\c".to_string()), r"/a/b\c");
}
}
#[cfg(not(windows))]
#[test]
fn test_display_user_leaves_unix_paths_alone() {
assert_eq!(Path::new("/usr/local/bin").display_user(), "/usr/local/bin");
assert_eq!(Path::new(r"weird\name").display_user(), r"weird\name");
assert_eq!(Path::new(r"/a/b\c").display_user(), r"/a/b\c");
if dirs::HOME.as_os_str() != "/" {
assert_eq!(dirs::HOME.join("proj").display_user(), "~/proj");
}
}
#[test]
fn test_is_posix_shell_program() {
assert!(is_posix_shell_program(Path::new("bash")));
assert!(is_posix_shell_program(Path::new("bash.exe")));
assert!(is_posix_shell_program(Path::new("BASH.EXE")));
assert!(is_posix_shell_program(Path::new(
r"C:\Program Files\Git\bin\bash.exe"
)));
assert!(is_posix_shell_program(Path::new("/usr/bin/bash")));
assert!(is_posix_shell_program(Path::new("sh")));
assert!(is_posix_shell_program(Path::new("zsh")));
assert!(is_posix_shell_program(Path::new("fish")));
assert!(is_posix_shell_program(Path::new("ash")));
assert!(!is_posix_shell_program(Path::new("cmd")));
assert!(!is_posix_shell_program(Path::new("cmd.exe")));
assert!(!is_posix_shell_program(Path::new("powershell")));
assert!(!is_posix_shell_program(Path::new("pwsh.exe")));
assert!(!is_posix_shell_program(Path::new("rustc")));
assert!(!is_posix_shell_program(Path::new("")));
}
#[test]
fn test_command_mode_script_payload() {
let payload = |p: &str| command_mode_script_payload(Path::new(p));
for posix in [
"bash",
"sh",
"zsh",
"ksh",
"dash",
"ash",
"/usr/bin/bash",
"BASH.EXE",
] {
assert_eq!(payload(posix), Some(r#""$0" "$@""#), "{posix}");
}
for fish in ["fish", "fish.exe", "/usr/bin/fish"] {
assert_eq!(payload(fish), Some("$argv"), "{fish}");
}
for other in ["cmd", "cmd.exe", "pwsh", "powershell.exe", "rustc", ""] {
assert_eq!(payload(other), None, "{other}");
}
}
#[test]
fn test_is_cmd_shell_program() {
assert!(is_cmd_shell_program(Path::new("cmd")));
assert!(is_cmd_shell_program(Path::new("cmd.exe")));
assert!(is_cmd_shell_program(Path::new("CMD.EXE")));
assert!(is_cmd_shell_program(Path::new(
r"C:\Windows\System32\cmd.exe"
)));
assert!(!is_cmd_shell_program(Path::new("bash")));
assert!(!is_cmd_shell_program(Path::new("bash.exe")));
assert!(!is_cmd_shell_program(Path::new("powershell")));
assert!(!is_cmd_shell_program(Path::new("pwsh.exe")));
assert!(!is_cmd_shell_program(Path::new("cmd.com")));
assert!(!is_cmd_shell_program(Path::new("")));
}
#[test]
fn test_is_powershell_program() {
assert!(is_powershell_program(Path::new("pwsh")));
assert!(is_powershell_program(Path::new("pwsh.exe")));
assert!(is_powershell_program(Path::new("PWSH.EXE")));
assert!(is_powershell_program(Path::new("powershell")));
assert!(is_powershell_program(Path::new("powershell.exe")));
assert!(is_powershell_program(Path::new(
r"C:\Program Files\PowerShell\7\pwsh.exe"
)));
assert!(!is_powershell_program(Path::new("cmd")));
assert!(!is_powershell_program(Path::new("bash")));
assert!(!is_powershell_program(Path::new("")));
}
#[test]
fn test_inject_powershell_no_profile() {
let inject = |args: &[&str]| {
let mut v = sv(args);
inject_powershell_no_profile(&mut v);
v
};
assert_eq!(
inject(&["pwsh", "-Command"]),
sv(&["pwsh", "-NoProfile", "-Command"])
);
assert_eq!(
inject(&["powershell", "-c"]),
sv(&["powershell", "-NoProfile", "-c"])
);
assert_eq!(
inject(&["pwsh.exe", "-NoLogo", "-Command"]),
sv(&["pwsh.exe", "-NoProfile", "-NoLogo", "-Command"])
);
assert_eq!(inject(&["cmd", "/c"]), sv(&["cmd", "/c"]));
assert_eq!(inject(&["bash", "-c"]), sv(&["bash", "-c"]));
assert_eq!(inject(&[]), sv(&[]));
assert_eq!(
inject(&["pwsh", "-NoProfile", "-Command"]),
sv(&["pwsh", "-NoProfile", "-Command"])
);
assert_eq!(
inject(&["pwsh", "-noprofile", "-c"]),
sv(&["pwsh", "-noprofile", "-c"])
);
assert_eq!(inject(&["pwsh", "-nop", "-c"]), sv(&["pwsh", "-nop", "-c"]));
assert_eq!(
inject(&["pwsh", "/NoProfile", "-c"]),
sv(&["pwsh", "/NoProfile", "-c"])
);
assert_eq!(
inject(&["pwsh", "-Command", "-nop"]),
sv(&["pwsh", "-NoProfile", "-Command", "-nop"])
);
assert_eq!(
inject(&["pwsh", "-File", "script.ps1", "-nop"]),
sv(&["pwsh", "-NoProfile", "-File", "script.ps1", "-nop"])
);
assert_eq!(
inject(&["pwsh", "-NoProfileLoadTime", "-c"]),
sv(&["pwsh", "-NoProfile", "-NoProfileLoadTime", "-c"])
);
assert_eq!(
inject(&["pwsh", "-NoLogo", "-c"]),
sv(&["pwsh", "-NoProfile", "-NoLogo", "-c"])
);
}
#[test]
fn test_cmd_verbatim_args() {
let c = || "/c".to_string();
assert_eq!(
cmd_verbatim_args(&[c()], r#"uv run python -c "import x""#, &[]),
sv(&["/s", "/c", r#""uv run python -c "import x"""#])
);
assert_eq!(
cmd_verbatim_args(&[c()], "echo hi", &[]),
sv(&["/s", "/c", r#""echo hi""#])
);
assert_eq!(
cmd_verbatim_args(&[c()], "proxy", &["a b".to_string(), "c".to_string()]),
sv(&["/s", "/c", r#""proxy "a b" c""#])
);
assert_eq!(
cmd_verbatim_args(&[c()], "type", &[r".\test dir\file.txt".to_string()]),
sv(&["/s", "/c", r#""type ".\test dir\file.txt"""#])
);
assert_eq!(
cmd_verbatim_args(&["/s".to_string(), c()], "echo hi", &[]),
sv(&["/s", "/c", r#""echo hi""#])
);
}
#[test]
fn test_quote_arg_for_cmd_body() {
assert_eq!(quote_arg_for_cmd_body("plain"), "plain");
assert_eq!(quote_arg_for_cmd_body("a b"), r#""a b""#);
assert_eq!(quote_arg_for_cmd_body(""), r#""""#);
assert_eq!(quote_arg_for_cmd_body(r#"a"b"#), r#""a\"b""#);
assert_eq!(quote_arg_for_cmd_body(r"a b\"), r#""a b\\""#);
assert_eq!(quote_arg_for_cmd_body(r"a\b c"), r#""a\b c""#);
assert_eq!(quote_arg_for_cmd_body("a&b"), r#""a&b""#);
assert_eq!(quote_arg_for_cmd_body("foo|bar"), r#""foo|bar""#);
assert_eq!(quote_arg_for_cmd_body("a>b"), r#""a>b""#);
}
#[test]
#[cfg(windows)]
fn test_cmd_verbatim_command() {
let c = cmd_verbatim_command("cmd", &["/c".to_string()], r#"echo "a b""#).unwrap();
assert_eq!(c.get_program().to_str(), Some("cmd"));
let args: Vec<String> = c
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(
args,
vec![
"/s".to_string(),
"/c".to_string(),
r#""echo "a b"""#.to_string()
]
);
assert!(cmd_verbatim_command("cmd", &["/k".to_string()], "echo hi").is_some());
assert!(cmd_verbatim_command("bash", &["-c".to_string()], "echo hi").is_none());
assert!(cmd_verbatim_command("cmd", &[], "echo hi").is_none());
}
#[test]
fn test_split_shell_command_bare_names() {
assert_eq!(split_shell_command("bash -c").unwrap(), sv(&["bash", "-c"]));
assert_eq!(split_shell_command("sh -c").unwrap(), sv(&["sh", "-c"]));
assert_eq!(
split_shell_command("sh -o errexit -c").unwrap(),
sv(&["sh", "-o", "errexit", "-c"])
);
}
#[test]
fn test_split_shell_command_empty() {
assert_eq!(split_shell_command("").unwrap(), sv(&[]));
assert_eq!(split_shell_command(" ").unwrap(), sv(&[]));
}
#[test]
fn test_split_shell_command_quoted_path_with_spaces() {
assert_eq!(
split_shell_command("\"C:/Program Files/Git/bin/bash.exe\" -c").unwrap(),
sv(&["C:/Program Files/Git/bin/bash.exe", "-c"])
);
}
#[cfg(windows)]
#[test]
fn test_split_shell_command_windows_backslash_is_literal() {
assert_eq!(
split_shell_command(r"C:\msys64\usr\bin\bash.exe -c").unwrap(),
sv(&[r"C:\msys64\usr\bin\bash.exe", "-c"])
);
assert_eq!(
split_shell_command("\"C:\\Program Files\\Git\\bin\\bash.exe\" -c").unwrap(),
sv(&[r"C:\Program Files\Git\bin\bash.exe", "-c"])
);
}
#[cfg(windows)]
#[test]
fn test_split_shell_command_windows_unquoted_space_splits() {
assert_eq!(
split_shell_command(r"C:/Program Files/Git/bin/bash.exe -c").unwrap(),
sv(&["C:/Program", "Files/Git/bin/bash.exe", "-c"])
);
}
#[cfg(windows)]
#[test]
fn test_split_shell_command_windows_double_quote_is_literal() {
assert_eq!(
split_shell_command("\"a\"\"b\" c").unwrap(),
sv(&["a\"b", "c"])
);
}
#[cfg(windows)]
#[test]
fn test_split_shell_command_windows_unbalanced_quote_errs() {
assert!(split_shell_command("\"unterminated").is_err());
}
#[cfg(not(windows))]
#[test]
fn test_split_shell_command_unix_posix_semantics() {
assert_eq!(
split_shell_command(r"bash\ script -c").unwrap(),
sv(&["bash script", "-c"])
);
assert_eq!(split_shell_command("'a b' c").unwrap(), sv(&["a b", "c"]));
}
#[test]
fn test_unix_path_to_windows_msys_drive_paths() {
assert_eq!(unix_path_to_windows("/c/foo").as_deref(), Some(r"C:\foo"));
assert_eq!(unix_path_to_windows("/C/foo").as_deref(), Some(r"C:\foo"));
assert_eq!(
unix_path_to_windows("/c/Program Files/Git").as_deref(),
Some(r"C:\Program Files\Git")
);
assert_eq!(unix_path_to_windows("/c").as_deref(), Some(r"C:\"));
assert_eq!(unix_path_to_windows("/c/").as_deref(), Some(r"C:\"));
}
#[test]
fn test_unix_path_to_windows_cygdrive_paths() {
assert_eq!(
unix_path_to_windows("/cygdrive/c/foo").as_deref(),
Some(r"C:\foo")
);
assert_eq!(unix_path_to_windows("/cygdrive/c").as_deref(), Some(r"C:\"));
assert_eq!(unix_path_to_windows("/cygdrive"), None);
assert_eq!(unix_path_to_windows("/cygdrive2/c/x"), None);
}
#[test]
fn test_unix_path_to_windows_already_windows() {
assert_eq!(
unix_path_to_windows("C:/already").as_deref(),
Some(r"C:\already")
);
assert_eq!(
unix_path_to_windows(r"C:\already").as_deref(),
Some(r"C:\already")
);
}
#[test]
fn test_unix_path_to_windows_unc() {
assert_eq!(
unix_path_to_windows("//server/share/dir").as_deref(),
Some(r"\\server\share\dir")
);
assert_eq!(unix_path_to_windows("//"), None);
}
#[test]
fn test_unix_path_to_windows_no_windows_equivalent() {
assert_eq!(unix_path_to_windows("/usr/bin"), None);
assert_eq!(unix_path_to_windows("/mingw64/bin"), None);
assert_eq!(unix_path_to_windows("relative/x"), None);
assert_eq!(unix_path_to_windows(""), None);
assert_eq!(unix_path_to_windows("/cc/foo"), None);
}
#[test]
#[cfg(windows)]
fn test_is_bash_basename_accepts_bash_variants() {
use std::ffi::OsStr;
assert!(is_bash_basename(OsStr::new("bash")));
assert!(is_bash_basename(OsStr::new("bash.exe")));
assert!(is_bash_basename(OsStr::new("BASH.EXE")));
assert!(is_bash_basename(OsStr::new(
r"C:\Program Files\Git\bin\bash.exe"
)));
assert!(is_bash_basename(OsStr::new("/usr/bin/bash")));
}
#[test]
#[cfg(windows)]
fn test_is_bash_basename_rejects_other_shells() {
use std::ffi::OsStr;
assert!(!is_bash_basename(OsStr::new("sh")));
assert!(!is_bash_basename(OsStr::new("zsh.exe")));
assert!(!is_bash_basename(OsStr::new("fish")));
assert!(!is_bash_basename(OsStr::new("dash")));
assert!(!is_bash_basename(OsStr::new("cmd.exe")));
assert!(!is_bash_basename(OsStr::new("bashfoo")));
}
#[test]
#[cfg(windows)]
fn test_is_wsl_launcher_bash_detects_system32() {
assert!(is_wsl_launcher_bash(Path::new(
r"C:\Windows\System32\bash.exe"
)));
assert!(is_wsl_launcher_bash(Path::new(
r"C:\WINDOWS\system32\bash.exe"
)));
assert!(is_wsl_launcher_bash(Path::new(
r"D:\Windows\System32\bash.exe"
)));
}
#[test]
#[cfg(windows)]
fn test_is_wsl_launcher_bash_detects_windows_apps() {
assert!(is_wsl_launcher_bash(Path::new(
r"C:\Users\me\AppData\Local\Microsoft\WindowsApps\bash.exe"
)));
assert!(is_wsl_launcher_bash(Path::new(
"C:/Users/me/AppData/Local/Microsoft/WindowsApps/bash.exe"
)));
}
#[test]
#[cfg(windows)]
fn test_is_wsl_launcher_bash_accepts_real_bash() {
assert!(!is_wsl_launcher_bash(Path::new(
r"C:\Program Files\Git\bin\bash.exe"
)));
assert!(!is_wsl_launcher_bash(Path::new(
r"C:\Program Files\Git\usr\bin\bash.exe"
)));
assert!(!is_wsl_launcher_bash(Path::new(
r"C:\msys64\usr\bin\bash.exe"
)));
assert!(!is_wsl_launcher_bash(Path::new(
r"C:\Users\me\scoop\apps\git\current\bin\bash.exe"
)));
}
#[test]
#[cfg(windows)]
fn test_bash_candidates_includes_program_files() {
let env = std::collections::BTreeMap::new();
let candidates = bash_candidates(&env);
assert!(candidates.contains(&PathBuf::from(r"C:\Program Files\Git\bin\bash.exe")));
assert!(candidates.contains(&PathBuf::from(r"C:\Program Files (x86)\Git\bin\bash.exe")));
}
#[test]
#[cfg(windows)]
fn test_bash_candidates_includes_msys2() {
let env = std::collections::BTreeMap::new();
let candidates = bash_candidates(&env);
assert!(candidates.contains(&PathBuf::from(r"C:\msys64\usr\bin\bash.exe")));
assert!(candidates.contains(&PathBuf::from(r"C:\msys32\usr\bin\bash.exe")));
}
#[test]
#[cfg(windows)]
fn test_bash_candidates_uses_localappdata_from_env() {
let mut env = std::collections::BTreeMap::new();
env.insert(
"LOCALAPPDATA".to_string(),
r"C:\Users\me\AppData\Local".to_string(),
);
let candidates = bash_candidates(&env);
assert!(candidates.contains(&PathBuf::from(
r"C:\Users\me\AppData\Local\Programs\Git\bin\bash.exe"
)));
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_uses_mise_bash_path_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let bash_path = tmp.path().join("custom-bash.exe");
std::fs::write(&bash_path, b"").expect("write fake bash");
let mut env = env_with_path(r"C:\Windows\System32;C:\Program Files\Git\bin");
env.insert(
"MISE_BASH_PATH".to_string(),
bash_path.to_string_lossy().into_owned(),
);
let resolved = resolve_posix_shell_program_path(std::ffi::OsStr::new("bash"), &env)
.expect("override should resolve");
assert_eq!(PathBuf::from(&resolved), bash_path);
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_override_beats_unix_form_path_gate() {
let tmp = tempfile::tempdir().expect("tempdir");
let bash_path = tmp.path().join("custom-bash.exe");
std::fs::write(&bash_path, b"").expect("write fake bash");
let mut env = env_with_path("/c/foo:/d/bar");
env.insert(
"MISE_BASH_PATH".to_string(),
bash_path.to_string_lossy().into_owned(),
);
let resolved = resolve_posix_shell_program_path(std::ffi::OsStr::new("bash"), &env)
.expect("override should resolve");
assert_eq!(PathBuf::from(&resolved), bash_path);
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_skips_when_not_posix_shell() {
let env = env_with_path(r"C:\Windows\System32");
assert!(resolve_posix_shell_program_path(std::ffi::OsStr::new("cmd.exe"), &env).is_none());
assert!(
resolve_posix_shell_program_path(std::ffi::OsStr::new("notepad.exe"), &env).is_none()
);
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_skips_when_path_already_unix() {
let env = env_with_path("/c/foo:/d/bar");
assert!(resolve_posix_shell_program_path(std::ffi::OsStr::new("bash"), &env).is_none());
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_honors_explicit_forward_slash_path() {
let env = env_with_path(r"C:\Windows\System32;C:\Program Files\Git\bin");
assert!(
resolve_posix_shell_program_path(
std::ffi::OsStr::new("C:/msys64/usr/bin/bash.exe"),
&env
)
.is_none()
);
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_honors_explicit_path_backslashes() {
let env = env_with_path(r"C:\Windows\System32;C:\Program Files\Git\bin");
assert!(
resolve_posix_shell_program_path(
std::ffi::OsStr::new(r"C:\msys64\usr\bin\bash.exe"),
&env
)
.is_none()
);
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_honors_explicit_relative_path() {
let env = env_with_path(r"C:\Windows\System32;C:\Program Files\Git\bin");
assert!(resolve_posix_shell_program_path(std::ffi::OsStr::new("bin/bash"), &env).is_none());
}
#[test]
#[cfg(windows)]
fn test_resolve_posix_shell_program_path_honors_explicit_non_bash_shell_path() {
let env = env_with_path(r"C:\Windows\System32;C:\msys64\usr\bin");
assert!(
resolve_posix_shell_program_path(
std::ffi::OsStr::new(r"C:\msys64\usr\bin\zsh.exe"),
&env
)
.is_none()
);
}
#[test]
#[cfg(windows)]
fn test_program_has_directory_component_detects_explicit_paths() {
use std::ffi::OsStr;
assert!(program_has_directory_component(OsStr::new(
"C:/msys64/usr/bin/bash.exe"
)));
assert!(program_has_directory_component(OsStr::new(
r"C:\msys64\usr\bin\bash.exe"
)));
assert!(program_has_directory_component(OsStr::new("./bash")));
assert!(program_has_directory_component(OsStr::new("bin/bash")));
assert!(program_has_directory_component(OsStr::new("/usr/bin/bash")));
}
#[test]
#[cfg(windows)]
fn test_program_has_directory_component_rejects_bare_names() {
use std::ffi::OsStr;
assert!(!program_has_directory_component(OsStr::new("bash")));
assert!(!program_has_directory_component(OsStr::new("bash.exe")));
assert!(!program_has_directory_component(OsStr::new("BASH.EXE")));
}
}