#[derive(Debug, Clone, PartialEq)]
pub enum ShellType {
Bash,
Cmd,
PowerShell,
Unknown,
}
pub fn detect_shell_type(command: &str) -> ShellType {
if has_cmd_features(command) {
return ShellType::Cmd;
}
if has_powershell_features(command) {
return ShellType::PowerShell;
}
ShellType::Bash
}
fn has_cmd_features(cmd: &str) -> bool {
let lower = cmd.to_lowercase();
if cmd.contains('%') {
let bytes = cmd.as_bytes();
let mut first_pct = None;
for (i, &b) in bytes.iter().enumerate() {
if b == b'%' {
if first_pct.is_some() {
return true;
}
first_pct = Some(i);
}
}
}
if lower.contains(">nul") || lower.contains("2>nul") {
return true;
}
let first_word = cmd.split_whitespace().next().unwrap_or("").to_lowercase();
matches!(
first_word.as_str(),
"dir" | "set" | "cls" | "ver" | "vol" | "type" | "copy" | "move" | "del"
)
}
fn has_powershell_features(cmd: &str) -> bool {
let lower = cmd.to_lowercase();
let first_word = lower.split_whitespace().next().unwrap_or("");
if matches!(
first_word,
"powershell" | "pwsh" | "powershell_ise" | "iex" | "iwr" | "sal"
) {
return true;
}
if lower.contains("$env:") {
return true;
}
const PATTERNS: &[&str] = &[
"write-",
"get-",
"invoke-",
"set-",
"new-",
"remove-",
"select-",
"where-",
"foreach-",
"out-",
"convertto-",
"start-",
"stop-",
"test-",
"add-",
"import-",
"export-",
];
for p in PATTERNS {
if lower.contains(p) {
return true;
}
}
if lower.contains("@(") {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_bash_simple() {
assert_eq!(detect_shell_type("ls -la"), ShellType::Bash);
}
#[test]
fn detect_bash_compound() {
assert_eq!(detect_shell_type("ls && echo done"), ShellType::Bash);
}
#[test]
fn detect_cmd_dir() {
assert_eq!(detect_shell_type("dir"), ShellType::Cmd);
}
#[test]
fn detect_cmd_var() {
assert_eq!(detect_shell_type("echo %PATH%"), ShellType::Cmd);
}
#[test]
fn detect_cmd_nul() {
assert_eq!(detect_shell_type("echo hello >nul"), ShellType::Cmd);
}
#[test]
fn detect_powershell_env() {
assert_eq!(detect_shell_type("echo $env:PATH"), ShellType::PowerShell);
}
#[test]
fn detect_powershell_cmdlet() {
assert_eq!(
detect_shell_type("Get-ChildItem -Path C:\\"),
ShellType::PowerShell
);
}
#[test]
fn detect_powershell_array() {
assert_eq!(detect_shell_type("@(1,2,3)"), ShellType::PowerShell);
}
#[test]
fn detect_bash_stderr_redirect() {
assert_eq!(detect_shell_type("ls 2>/dev/null"), ShellType::Bash);
}
#[test]
fn detect_bash_command_substitution() {
assert_eq!(detect_shell_type("echo $(whoami)"), ShellType::Bash);
}
#[test]
fn a05_lowercase_cmdlet_detected_as_powershell() {
assert_eq!(
detect_shell_type("get-childitem -path C:\\"),
ShellType::PowerShell
);
assert_eq!(
detect_shell_type("remove-item -recurse -force C:\\x"),
ShellType::PowerShell
);
}
#[test]
fn a05_uppercase_env_detected_as_powershell() {
assert_eq!(detect_shell_type("echo $ENV:PATH"), ShellType::PowerShell);
}
#[test]
fn a05_powershell_launcher_first_word() {
assert_eq!(
detect_shell_type("powershell -ec SGVsbG8="),
ShellType::PowerShell
);
assert_eq!(
detect_shell_type("PWSH -NoProfile -Command ls"),
ShellType::PowerShell
);
}
#[test]
fn a05_pure_ps_alias_first_word() {
assert_eq!(detect_shell_type("IEX 'evil'"), ShellType::PowerShell);
assert_eq!(detect_shell_type("iwr http://x"), ShellType::PowerShell);
}
}