use std::path::Path;
use std::env;
pub fn detect_user_shell() -> String {
if let Ok(shell) = env::var("SHELL") {
if is_valid_shell(&shell) {
log::info!("✓ Detected shell from $SHELL environment variable: {}", shell);
return shell;
}
log::warn!(
"⚠ $SHELL={} is invalid (not found or not executable), falling back to defaults",
shell
);
}
#[cfg(target_os = "windows")]
{
detect_windows_shell()
}
#[cfg(not(target_os = "windows"))]
{
detect_unix_shell()
}
}
pub fn validate_shell_path(path: &str) -> Result<(), String> {
if is_valid_shell(path) {
Ok(())
} else {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(format!("Shell does not exist: {}", path));
}
if !path_obj.is_file() {
return Err(format!("Shell path is not a file: {}", path));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path_obj) {
let mode = metadata.permissions().mode();
if mode & 0o111 == 0 {
return Err(format!("Shell is not executable: {}", path));
}
}
}
Err(format!("Shell validation failed: {}", path))
}
}
fn is_valid_shell(shell_path: &str) -> bool {
let path = Path::new(shell_path);
if !path.exists() {
log::debug!("Shell validation failed: {} does not exist", shell_path);
return false;
}
if !path.is_file() {
log::debug!("Shell validation failed: {} is not a regular file", shell_path);
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let mode = metadata.permissions().mode();
if mode & 0o111 == 0 {
log::debug!(
"Shell validation failed: {} has no execute permissions (mode: {:o})",
shell_path,
mode
);
return false;
}
} else {
log::debug!("Shell validation failed: cannot read metadata for {}", shell_path);
return false;
}
}
#[cfg(windows)]
{
if let Some(ext) = path.extension() {
let ext_lower = ext.to_string_lossy().to_lowercase();
if !["exe", "bat", "cmd", "ps1"].contains(&ext_lower.as_str()) {
log::debug!(
"Shell validation failed: {} has non-executable extension .{}",
shell_path,
ext_lower
);
return false;
}
}
}
true
}
#[cfg(target_os = "windows")]
fn detect_windows_shell() -> String {
let candidates: Vec<Option<String>> = vec![
env::var("WSL_DISTRO_NAME")
.ok()
.and_then(|distro_name| {
log::info!("🐧 WSL environment detected: {}", distro_name);
which::which("bash").ok()
})
.map(|p| p.to_string_lossy().to_string()),
which::which("bash").ok().map(|p| {
let path = p.to_string_lossy().to_string();
log::debug!("Found bash.exe in PATH: {}", path);
path
}),
which::which("pwsh").ok().map(|p| {
let path = p.to_string_lossy().to_string();
log::debug!("Found pwsh.exe in PATH: {}", path);
path
}),
which::which("powershell").ok().map(|p| {
let path = p.to_string_lossy().to_string();
log::debug!("Found powershell.exe in PATH: {}", path);
path
}),
Some("cmd.exe".to_string()),
];
for candidate in candidates.into_iter().flatten() {
if is_valid_shell(&candidate) {
log::info!("✓ Selected Windows shell: {}", candidate);
return candidate;
}
}
log::error!("⚠ No valid shell found on Windows! Using powershell.exe as emergency fallback");
"powershell.exe".to_string()
}
#[cfg(not(target_os = "windows"))]
fn detect_unix_shell() -> String {
let candidates = vec![
"/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh", ];
for candidate in candidates {
if is_valid_shell(candidate) {
log::info!("✓ Selected Unix shell: {}", candidate);
return candidate.to_string();
}
}
log::error!(
"🚨 CRITICAL: No valid shell found on Unix system! This violates POSIX compliance."
);
log::error!(" Using /bin/sh as emergency fallback (may not work)");
"/bin/sh".to_string()
}