use agtrace_types::ToolKind;
pub(crate) fn classify_execute_command(command: &str) -> Option<ToolKind> {
let cmd = command.trim();
let first_word = cmd.split_whitespace().next().unwrap_or("");
if is_search_command(cmd, first_word) {
return Some(ToolKind::Search);
}
if is_read_command(cmd, first_word) {
return Some(ToolKind::Read);
}
None
}
fn is_search_command(cmd: &str, first_word: &str) -> bool {
match first_word {
"grep" | "ag" | "ack" => true,
"rg" => {
!has_option(cmd, "--files")
&& !has_option(cmd, "-l")
&& !has_option(cmd, "--files-with-matches")
}
"bash" => {
if let Some(inner) = extract_bash_inner_command(cmd) {
let inner_first = inner.split_whitespace().next().unwrap_or("");
is_search_command(&inner, inner_first)
} else {
false
}
}
_ => false,
}
}
fn is_read_command(cmd: &str, first_word: &str) -> bool {
match first_word {
"cat" | "head" | "tail" | "less" | "more" => true,
"ls" | "find" | "tree" | "fd" => true,
"rg" if has_option(cmd, "--files")
|| has_option(cmd, "-l")
|| has_option(cmd, "--files-with-matches") =>
{
true
}
"wc" | "diff" | "stat" | "file" => true,
"sed" => {
!has_option(cmd, "-i") && !has_option(cmd, "--in-place")
}
"awk" => {
!cmd.contains(">")
}
"bash" => {
if let Some(inner) = extract_bash_inner_command(cmd) {
let inner_first = inner.split_whitespace().next().unwrap_or("");
is_read_command(&inner, inner_first)
} else {
false
}
}
_ => false,
}
}
fn extract_bash_inner_command(cmd: &str) -> Option<String> {
if let Some(idx) = cmd.find("-lc") {
let rest = cmd[idx + 3..].trim();
return Some(rest.to_string());
}
if let Some(idx) = cmd.find("-c") {
let rest = cmd[idx + 2..].trim();
return Some(rest.to_string());
}
None
}
fn has_option(cmd: &str, option: &str) -> bool {
cmd.split_whitespace().any(|word| {
word == option
|| word.starts_with(option)
&& (word.len() == option.len()
|| !word
.chars()
.nth(option.len())
.unwrap_or(' ')
.is_alphanumeric())
})
}
pub(crate) fn extract_search_pattern(command: &str) -> Option<String> {
let cmd = command.trim();
let first_word = cmd.split_whitespace().next()?;
match first_word {
"grep" | "rg" | "ag" | "ack" => {
let rest = cmd.strip_prefix(first_word)?.trim();
let mut chars = rest.chars().peekable();
let mut in_flags = true;
while in_flags && chars.peek().is_some() {
while chars.peek() == Some(&' ') {
chars.next();
}
if chars.peek() == Some(&'-') {
while chars.peek().is_some() && chars.peek() != Some(&' ') {
chars.next();
}
} else {
in_flags = false;
}
}
let remainder: String = chars.collect();
let remainder = remainder.trim();
if let Some(stripped) = remainder.strip_prefix('"') {
let end = stripped.find('"')?;
Some(stripped[..end].to_string())
} else if let Some(stripped) = remainder.strip_prefix('\'') {
let end = stripped.find('\'')?;
Some(stripped[..end].to_string())
} else {
remainder.split_whitespace().next().map(|s| s.to_string())
}
}
"bash" => {
extract_bash_inner_command(cmd).and_then(|inner| extract_search_pattern(&inner))
}
_ => None,
}
}
pub(crate) fn extract_file_path(command: &str) -> Option<String> {
let cmd = command.trim();
let first_word = cmd.split_whitespace().next()?;
match first_word {
"cat" | "head" | "tail" | "less" | "more" | "wc" | "file" => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
parts
.iter()
.rev()
.find(|&&part| !part.starts_with('-'))
.map(|s| s.to_string())
}
"sed" => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
parts
.iter()
.rev()
.find(|&&part| !part.starts_with('-') && !part.contains(',') && part != "sed")
.map(|s| s.to_string())
}
"grep" | "rg" | "ag" | "ack" => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.len() >= 3 {
parts.get(2).map(|s| s.to_string())
} else {
None
}
}
"bash" => {
extract_bash_inner_command(cmd).and_then(|inner| extract_file_path(&inner))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_search_commands() {
assert_eq!(
classify_execute_command("grep pattern file.txt"),
Some(ToolKind::Search)
);
assert_eq!(
classify_execute_command("rg -n \"context window\" docs -S"),
Some(ToolKind::Search)
);
assert_eq!(
classify_execute_command("ag \"pattern\" src/"),
Some(ToolKind::Search)
);
assert_eq!(
classify_execute_command("ack TODO crates/"),
Some(ToolKind::Search)
);
}
#[test]
fn test_classify_read_commands() {
assert_eq!(
classify_execute_command("cat file.txt"),
Some(ToolKind::Read)
);
assert_eq!(
classify_execute_command("sed -n '1,200p' file.txt"),
Some(ToolKind::Read)
);
assert_eq!(classify_execute_command("ls -la"), Some(ToolKind::Read));
assert_eq!(
classify_execute_command("rg --files docs"),
Some(ToolKind::Read)
);
assert_eq!(
classify_execute_command("rg -l pattern src/"),
Some(ToolKind::Read)
);
}
#[test]
fn test_classify_write_commands() {
assert_eq!(
classify_execute_command("sed -i 's/foo/bar/' file.txt"),
None
);
assert_eq!(classify_execute_command("mkdir -p dir"), None);
assert_eq!(classify_execute_command("rm file.txt"), None);
}
#[test]
fn test_classify_bash_wrapped() {
assert_eq!(
classify_execute_command("bash -lc cat file.txt"),
Some(ToolKind::Read)
);
assert_eq!(
classify_execute_command("bash -lc sed -n '1,100p' file.txt"),
Some(ToolKind::Read)
);
}
#[test]
fn test_sed_with_path_containing_dash_i() {
assert_eq!(
classify_execute_command("sed -n 1,200p packages/extension-inspector/src/App.tsx"),
Some(ToolKind::Read)
);
}
#[test]
fn test_extract_file_path() {
assert_eq!(
extract_file_path("cat file.txt"),
Some("file.txt".to_string())
);
assert_eq!(
extract_file_path("sed -n '1,100p' file.txt"),
Some("file.txt".to_string())
);
assert_eq!(
extract_file_path("grep pattern file.txt"),
Some("file.txt".to_string())
);
assert_eq!(extract_file_path("ls"), None);
}
#[test]
fn test_extract_file_path_bash_wrapped() {
assert_eq!(
extract_file_path("bash -lc cat file.txt"),
Some("file.txt".to_string())
);
}
#[test]
fn test_extract_search_pattern() {
assert_eq!(
extract_search_pattern("rg -n \"context window\" docs -S"),
Some("context window".to_string())
);
assert_eq!(
extract_search_pattern("grep pattern file.txt"),
Some("pattern".to_string())
);
assert_eq!(
extract_search_pattern("ag TODO src/"),
Some("TODO".to_string())
);
assert_eq!(
extract_search_pattern("rg --files docs"),
Some("docs".to_string())
);
}
}