#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExitOutcome {
Error,
Info(&'static str),
}
pub fn interpret_exit(cmd: &str, code: i32) -> ExitOutcome {
if code == 0 {
return ExitOutcome::Info("success");
}
let base = base_command(cmd);
match (base, code) {
("grep" | "egrep" | "fgrep" | "rg" | "ag" | "ack", 1) => {
ExitOutcome::Info("No matches found")
}
("diff" | "colordiff", 1) => ExitOutcome::Info("Files differ"),
("find" | "fd", 1) => ExitOutcome::Info("Some paths were inaccessible"),
("test" | "[", 1) => ExitOutcome::Info("Condition is false"),
_ => ExitOutcome::Error,
}
}
pub fn is_silent_command(cmd: &str) -> bool {
matches!(
base_command(cmd),
"mv" | "cp"
| "rm"
| "mkdir"
| "rmdir"
| "touch"
| "chmod"
| "chown"
| "chgrp"
| "ln"
| "unlink"
| "install"
| "cd"
| "export"
| "unset"
)
}
pub fn is_blocking_sleep(cmd: &str) -> Option<String> {
let trimmed = cmd.trim();
if trimmed.contains('&') && !trimmed.contains("&&") {
return None;
}
let first = trimmed.split([';', '|']).next().unwrap_or(trimmed).trim();
let first = first.split("&&").next().unwrap_or(first).trim();
let base = base_command(first);
if base != "sleep" {
return None;
}
let arg = first
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<f64>().ok())?;
if arg < 2.0 {
return None; }
let rest = trimmed
.trim_start_matches(first)
.trim_start_matches(|c: char| c == ';' || c == '&' || c.is_whitespace());
if rest.is_empty() {
Some(format!(
"Blocked: standalone sleep {arg}s. Use background=true for long waits."
))
} else {
Some(format!(
"Blocked: sleep {arg}s followed by: {rest}. \
Use background=true, or keep sleep under 2s."
))
}
}
pub fn base_command(cmd: &str) -> &str {
let trimmed = cmd.trim();
let before_pipe = trimmed.split('|').next().unwrap_or(trimmed).trim();
for token in before_pipe.split_whitespace() {
if token.contains('=') && !token.starts_with('-') {
continue;
}
return token.rsplit('/').next().unwrap_or(token);
}
trimmed
.split_whitespace()
.next()
.unwrap_or(trimmed)
.rsplit('/')
.next()
.unwrap_or(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_command_simple() {
assert_eq!(base_command("grep -r foo ."), "grep");
}
#[test]
fn base_command_env_prefix() {
assert_eq!(base_command("FOO=1 BAR=2 grep pattern"), "grep");
}
#[test]
fn base_command_absolute_path() {
assert_eq!(base_command("/usr/bin/grep foo"), "grep");
}
#[test]
fn base_command_pipe() {
assert_eq!(base_command("grep foo | head -5"), "grep");
}
#[test]
fn base_command_bare() {
assert_eq!(base_command("ls"), "ls");
}
#[test]
fn base_command_with_whitespace() {
assert_eq!(base_command(" diff a.txt b.txt "), "diff");
}
#[test]
fn grep_no_matches() {
assert_eq!(
interpret_exit("grep -r foo .", 1),
ExitOutcome::Info("No matches found")
);
}
#[test]
fn grep_real_error() {
assert_eq!(interpret_exit("grep -r foo .", 2), ExitOutcome::Error);
}
#[test]
fn rg_no_matches() {
assert_eq!(
interpret_exit("rg pattern", 1),
ExitOutcome::Info("No matches found")
);
}
#[test]
fn diff_files_differ() {
assert_eq!(
interpret_exit("diff a.txt b.txt", 1),
ExitOutcome::Info("Files differ")
);
}
#[test]
fn diff_real_error() {
assert_eq!(interpret_exit("diff a.txt b.txt", 2), ExitOutcome::Error);
}
#[test]
fn find_partial() {
assert_eq!(
interpret_exit("find / -name '*.rs'", 1),
ExitOutcome::Info("Some paths were inaccessible")
);
}
#[test]
fn test_condition_false() {
assert_eq!(
interpret_exit("test -f /nonexistent", 1),
ExitOutcome::Info("Condition is false")
);
}
#[test]
fn bracket_condition_false() {
assert_eq!(
interpret_exit("[ -f /nonexistent ]", 1),
ExitOutcome::Info("Condition is false")
);
}
#[test]
fn unknown_command_error() {
assert_eq!(interpret_exit("cargo build", 1), ExitOutcome::Error);
}
#[test]
fn exit_zero_always_info() {
assert_eq!(interpret_exit("anything", 0), ExitOutcome::Info("success"));
}
#[test]
fn silent_mv() {
assert!(is_silent_command("mv a b"));
}
#[test]
fn silent_mkdir() {
assert!(is_silent_command("mkdir -p /tmp/foo"));
}
#[test]
fn not_silent_grep() {
assert!(!is_silent_command("grep foo"));
}
#[test]
fn not_silent_ls() {
assert!(!is_silent_command("ls -la"));
}
#[test]
fn sleep_standalone_blocked() {
assert!(is_blocking_sleep("sleep 5").is_some());
}
#[test]
fn sleep_with_follow_up_blocked() {
assert!(is_blocking_sleep("sleep 5 && echo done").is_some());
}
#[test]
fn sleep_short_allowed() {
assert!(is_blocking_sleep("sleep 1").is_none());
}
#[test]
fn sleep_sub_second_allowed() {
assert!(is_blocking_sleep("sleep 0.5").is_none());
}
#[test]
fn sleep_backgrounded_allowed() {
assert!(is_blocking_sleep("sleep 10 &").is_none());
}
#[test]
fn not_sleep_command() {
assert!(is_blocking_sleep("echo hello").is_none());
}
#[test]
fn sleep_not_first_command() {
assert!(is_blocking_sleep("echo | sleep 10").is_none());
}
}