1
0
Files
shell/src/judge.rs
2026-02-27 19:49:18 +09:00

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));
}
}