use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
static SKIP_APP_EXECUTION_ALIAS: AtomicBool = AtomicBool::new(true);
pub fn set_skip_app_execution_alias(skip: bool) {
SKIP_APP_EXECUTION_ALIAS.store(skip, Ordering::Relaxed);
}
fn skip_app_execution_alias() -> bool {
SKIP_APP_EXECUTION_ALIAS.load(Ordering::Relaxed)
}
pub fn select_windows_shell() -> String {
pick_shell(skip_app_execution_alias(), &default_candidates(), |c| {
resolve_shell_candidate(c)
})
.unwrap_or_else(|| std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()))
}
fn default_candidates() -> Vec<String> {
let mut candidates: Vec<String> = Vec::new();
for var in ["ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"] {
if let Ok(base) = std::env::var(var) {
if !base.is_empty() {
candidates.push(format!(r"{base}\PowerShell\7\pwsh.exe"));
}
}
}
candidates.push("pwsh.exe".to_string());
candidates.push("powershell.exe".to_string());
candidates.push(r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe".to_string());
candidates
}
fn pick_shell<F>(skip_alias: bool, candidates: &[String], mut resolve: F) -> Option<String>
where
F: FnMut(&str) -> Option<PathBuf>,
{
for candidate in candidates {
if let Some(resolved) = resolve(candidate) {
if skip_alias && is_app_execution_alias(&resolved) {
continue;
}
return Some(resolved.to_string_lossy().into_owned());
}
}
None
}
pub(crate) fn resolve_shell_candidate(cmd: &str) -> Option<PathBuf> {
if cmd.contains('\\') || cmd.contains('/') {
let p = PathBuf::from(cmd);
return p.is_file().then_some(p);
}
let path_var = std::env::var("PATH").ok()?;
path_var
.split(';')
.filter(|d| !d.is_empty())
.find_map(|dir| {
let full = Path::new(dir).join(cmd);
full.is_file().then_some(full)
})
}
pub(crate) fn is_app_execution_alias(path: &Path) -> bool {
std::fs::metadata(path)
.map(|m| m.len() == 0)
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn detects_zero_byte_stub() {
let dir = tempfile::tempdir().unwrap();
let alias = dir.path().join("pwsh.exe");
std::fs::File::create(&alias).unwrap();
assert!(is_app_execution_alias(&alias));
let real = dir.path().join("powershell.exe");
let mut f = std::fs::File::create(&real).unwrap();
f.write_all(b"MZ\x90\x00").unwrap();
drop(f);
assert!(!is_app_execution_alias(&real));
}
#[test]
fn resolve_candidate_honors_explicit_path_and_misses() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("powershell.exe");
std::fs::write(&real, b"MZ").unwrap();
let resolved = resolve_shell_candidate(&real.to_string_lossy()).unwrap();
assert_eq!(resolved, real);
assert!(
resolve_shell_candidate(&dir.path().join("missing.exe").to_string_lossy()).is_none()
);
}
#[test]
fn pick_shell_skips_alias_stub_only_when_flag_set() {
let dir = tempfile::tempdir().unwrap();
let alias = dir.path().join("pwsh.exe");
std::fs::File::create(&alias).unwrap();
let real = dir.path().join("powershell.exe");
std::fs::write(&real, b"MZ").unwrap();
let candidates = vec![
alias.to_string_lossy().into_owned(),
real.to_string_lossy().into_owned(),
];
let picked = pick_shell(true, &candidates, resolve_shell_candidate).unwrap();
assert_eq!(PathBuf::from(picked), real);
let picked = pick_shell(false, &candidates, resolve_shell_candidate).unwrap();
assert_eq!(PathBuf::from(picked), alias);
}
#[test]
fn pick_shell_returns_none_when_nothing_resolves() {
let candidates = vec!["nonexistent.exe".to_string()];
assert!(pick_shell(true, &candidates, |_| None).is_none());
}
}