pub use std::path::*;
use crate::dirs;
pub 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 home = dirs::HOME.to_string_lossy();
let home_str: &str = home.as_ref();
match cfg!(unix) && self.starts_with(home_str) && home != "/" {
true => self.to_string_lossy().replacen(home_str, "~", 1),
false => self.to_string_lossy().to_string(),
}
}
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 fn windows_path_list_to_unix(path_list: &str, drive_prefix: &str) -> String {
let mut out = String::with_capacity(path_list.len());
let mut first = true;
for entry in path_list.split(WINDOWS_PATH_SEP) {
if !first {
out.push(':');
}
append_single_windows_path_to_unix(&mut out, entry, drive_prefix);
first = false;
}
out
}
#[cfg_attr(not(windows), allow(dead_code))]
const WINDOWS_PATH_SEP: char = ';';
#[cfg_attr(not(windows), allow(dead_code))]
fn append_single_windows_path_to_unix(out: &mut String, entry: &str, drive_prefix: &str) {
if entry.is_empty() {
return;
}
if entry.starts_with('/') || entry.starts_with("\\\\") {
out.push_str(entry);
return;
}
let bytes = entry.as_bytes();
let is_canonical_drive = bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/');
let rest = if is_canonical_drive {
out.push_str(drive_prefix);
out.push('/');
out.push((bytes[0] as char).to_ascii_lowercase());
&entry[2..]
} else {
entry
};
for c in rest.chars() {
out.push(if c == '\\' { '/' } else { c });
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub 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 fn is_posix_shell_program(program: &Path) -> bool {
const POSIX_SHELLS: &[&str] = &["bash", "sh", "zsh", "fish", "ksh", "dash"];
let Some(stem) = program_stem(program) else {
return false;
};
POSIX_SHELLS.iter().any(|name| *name == stem)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub fn is_cmd_shell_program(program: &Path) -> bool {
program_stem(program).as_deref() == Some("cmd")
}
#[cfg_attr(not(windows), allow(dead_code))]
pub 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 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 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 fn is_cygwin_shell(program: &Path) -> bool {
let Some(s) = program.to_str() else {
return false;
};
s.split(['/', '\\']).any(|seg| {
seg.eq_ignore_ascii_case("cygwin")
|| seg.eq_ignore_ascii_case("cygwin64")
|| seg.eq_ignore_ascii_case("cygwin32")
})
}
#[cfg_attr(not(windows), allow(dead_code))]
pub 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 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 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
}
fn msys(s: &str) -> String {
windows_path_list_to_unix(s, "")
}
fn cygwin(s: &str) -> String {
windows_path_list_to_unix(s, "/cygdrive")
}
#[test]
fn test_windows_path_list_to_unix_basic() {
assert_eq!(msys(r"C:\foo;D:\bar"), "/c/foo:/d/bar");
}
#[test]
fn test_windows_path_list_to_unix_forward_slash() {
assert_eq!(msys("C:/foo;D:/bar"), "/c/foo:/d/bar");
}
#[test]
fn test_windows_path_list_to_unix_mixed_separators() {
assert_eq!(msys(r"C:\foo\bar;D:/baz/qux"), "/c/foo/bar:/d/baz/qux");
}
#[test]
fn test_windows_path_list_to_unix_passthrough_unix_entries() {
assert_eq!(msys("/usr/bin;C:\\foo;/c/bar"), "/usr/bin:/c/foo:/c/bar");
}
#[test]
fn test_windows_path_list_to_unix_passthrough_unc() {
assert_eq!(msys(r"\\?\C:\foo;C:\bar"), r"\\?\C:\foo:/c/bar");
}
#[test]
fn test_windows_path_list_to_unix_empty_entries() {
assert_eq!(msys("C:\\foo;"), "/c/foo:");
assert_eq!(msys(";C:\\foo"), ":/c/foo");
assert_eq!(msys(""), "");
}
#[test]
fn test_windows_path_list_to_unix_drive_letter_case() {
assert_eq!(msys(r"C:\foo"), "/c/foo");
assert_eq!(msys(r"c:\foo"), "/c/foo");
}
#[test]
fn test_windows_path_list_to_unix_program_files_with_spaces() {
assert_eq!(
msys(r"C:\Program Files\Git\bin"),
"/c/Program Files/Git/bin"
);
}
#[test]
fn test_windows_path_list_to_unix_bare_drive_letter_passthrough() {
assert_eq!(msys("C:"), "C:");
assert_eq!(msys("C:foo"), "C:foo");
}
#[test]
fn test_windows_path_list_to_unix_relative_paths_with_backslashes() {
assert_eq!(msys(r"node_modules\.bin"), "node_modules/.bin");
assert_eq!(msys(r".\bin"), "./bin");
assert_eq!(
msys(r"node_modules\.bin;C:\tools\bin"),
"node_modules/.bin:/c/tools/bin"
);
}
#[test]
fn test_windows_path_list_to_unix_single_entry() {
assert_eq!(msys(r"C:\foo"), "/c/foo");
}
#[test]
fn test_windows_path_list_to_unix_cygwin_basic() {
assert_eq!(cygwin(r"C:\foo;D:\bar"), "/cygdrive/c/foo:/cygdrive/d/bar");
}
#[test]
fn test_windows_path_list_to_unix_cygwin_forward_slash() {
assert_eq!(cygwin("C:/foo;D:/bar"), "/cygdrive/c/foo:/cygdrive/d/bar");
}
#[test]
fn test_windows_path_list_to_unix_cygwin_drive_letter_case() {
assert_eq!(cygwin(r"c:\foo"), "/cygdrive/c/foo");
}
#[test]
fn test_windows_path_list_to_unix_cygwin_program_files_with_spaces() {
assert_eq!(
cygwin(r"C:\Program Files\Git\bin"),
"/cygdrive/c/Program Files/Git/bin"
);
}
#[test]
fn test_windows_path_list_to_unix_cygwin_passthrough_unix_and_unc() {
assert_eq!(
cygwin(r"/usr/bin;\\?\C:\x;C:\y"),
r"/usr/bin:\\?\C:\x:/cygdrive/c/y"
);
}
#[test]
fn test_windows_path_list_to_unix_cygwin_empty_entries() {
assert_eq!(cygwin("C:\\foo;"), "/cygdrive/c/foo:");
}
#[test]
fn test_windows_path_list_to_unix_cygwin_relative_paths_unprefixed() {
assert_eq!(cygwin(r"node_modules\.bin"), "node_modules/.bin");
}
#[test]
fn test_windows_path_list_to_unix_custom_cygdrive_prefix() {
assert_eq!(
windows_path_list_to_unix(r"C:\foo;D:\bar", "/mnt"),
"/mnt/c/foo:/mnt/d/bar"
);
}
#[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("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_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_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 -c -o errexit").unwrap(),
sv(&["sh", "-c", "-o", "errexit"])
);
}
#[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_is_cygwin_shell_detects_cygwin_paths() {
assert!(is_cygwin_shell(Path::new(r"C:\cygwin64\bin\bash.exe")));
assert!(is_cygwin_shell(Path::new(r"C:\cygwin\bin\bash.exe")));
assert!(is_cygwin_shell(Path::new(
r"D:\tools\cygwin64\bin\bash.exe"
)));
assert!(is_cygwin_shell(Path::new("C:/cygwin64/bin/bash.exe")));
assert!(is_cygwin_shell(Path::new(r"C:\CygWin64\bin\BASH.EXE")));
}
#[test]
fn test_is_cygwin_shell_rejects_non_cygwin() {
assert!(!is_cygwin_shell(Path::new(
r"C:\Program Files\Git\bin\bash.exe"
)));
assert!(!is_cygwin_shell(Path::new(r"C:\msys64\usr\bin\bash.exe")));
assert!(!is_cygwin_shell(Path::new("bash")));
assert!(!is_cygwin_shell(Path::new(
r"C:\Users\me\scoop\apps\git\current\bin\bash.exe"
)));
assert!(!is_cygwin_shell(Path::new(
r"C:\my-cygwinish-tools\bash.exe"
)));
}
#[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")));
}
}