170 lines
4.7 KiB
Rust
170 lines
4.7 KiB
Rust
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<String>,
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|