use std::collections::HashSet; use std::env; use std::fs; const SHELL_BUILTINS: &[&str] = &[ ".", ":", "alias", "bg", "bind", "break", "builtin", "caller", "cd", "command", "compgen", "complete", "compopt", "continue", "declare", "dirs", "disown", "echo", "enable", "eval", "exec", "exit", "export", "false", "fc", "fg", "getopts", "hash", "help", "history", "jobs", "kill", "let", "local", "logout", "mapfile", "popd", "printf", "pushd", "pwd", "read", "readarray", "readonly", "return", "set", "shift", "shopt", "source", "suspend", "test", "times", "trap", "true", "type", "typeset", "ulimit", "umask", "unalias", "unset", "wait", ]; /// Cached command set for fast lookup without fork. pub struct CommandCache { commands: HashSet, } impl CommandCache { pub fn new() -> Self { let mut commands = HashSet::new(); // Shell builtins for &b in SHELL_BUILTINS { commands.insert(b.to_string()); } // Scan PATH directories if let Ok(path_var) = env::var("PATH") { for dir in path_var.split(':') { if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { if let Some(name) = entry.file_name().to_str() { commands.insert(name.to_string()); } } } } } Self { commands } } pub fn contains(&self, cmd: &str) -> bool { self.commands.contains(cmd) } } /// Determine whether user input should be executed as a shell command. pub fn is_command(input: &str, cache: &CommandCache) -> bool { let trimmed = input.trim(); if trimmed.is_empty() { return false; } // Shell operators: pipe, redirect, background, semicolon, logical operators if contains_shell_operator(trimmed) { return true; } // Variable assignment: FOO=bar if is_variable_assignment(trimmed) { return true; } // Extract first token let first_token = match trimmed.split_whitespace().next() { Some(t) => t, None => return false, }; // Lookup in cached command set (no fork) cache.contains(first_token) } fn contains_shell_operator(input: &str) -> bool { let mut in_single = false; let mut in_double = false; let mut prev = '\0'; for ch in input.chars() { match ch { '\'' if !in_double && prev != '\\' => in_single = !in_single, '"' if !in_single && prev != '\\' => in_double = !in_double, '|' | ';' if !in_single && !in_double => return true, '>' | '<' if !in_single && !in_double => return true, '&' if !in_single && !in_double => return true, _ => {} } prev = ch; } false } fn is_variable_assignment(input: &str) -> bool { if let Some(eq_pos) = input.find('=') { if eq_pos == 0 { return false; } let name = &input[..eq_pos]; if name.contains(' ') { return false; } let first = name.chars().next().unwrap(); if !first.is_alphabetic() && first != '_' { return false; } return name.chars().all(|c| c.is_alphanumeric() || c == '_'); } false } #[cfg(test)] mod tests { use super::*; fn cache() -> CommandCache { CommandCache::new() } #[test] fn test_builtins() { let c = cache(); assert!(is_command("cd /tmp", &c)); assert!(is_command("export FOO=bar", &c)); assert!(is_command("echo hello", &c)); assert!(is_command("pwd", &c)); } #[test] fn test_path_commands() { let c = cache(); assert!(is_command("ls", &c)); assert!(is_command("ls -la", &c)); assert!(is_command("cat /etc/hostname", &c)); } #[test] fn test_shell_operators() { let c = cache(); assert!(is_command("ls | grep foo", &c)); assert!(is_command("echo hello > /tmp/out", &c)); assert!(is_command("cat file1 && cat file2", &c)); } #[test] fn test_variable_assignment() { let c = cache(); assert!(is_command("FOO=bar", &c)); assert!(is_command("MY_VAR=hello", &c)); } #[test] fn test_ai_input() { let c = cache(); assert!(!is_command("macbookがフリーズする原因を調べて", &c)); assert!(!is_command("hello world what is this", &c)); assert!(!is_command("Rustでhello worldを書いて", &c)); } #[test] fn test_empty() { let c = cache(); assert!(!is_command("", &c)); assert!(!is_command(" ", &c)); } }