#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BashFailureKind {
ModelError,
Environmental,
Unknown,
}
const MODEL_ERROR_MARKERS: &[&str] = &[
"syntax error",
"unexpected token",
"unexpected end of file",
"parse error",
"unbound variable",
"bad substitution",
"command not found",
"no such command",
"invalid option",
"illegal option",
"unknown option",
"unrecognized option",
"missing operand",
"too many arguments",
];
const ENVIRONMENTAL_MARKERS: &[&str] = &[
"no such file or directory",
"cannot access",
"does not exist",
"permission denied",
"operation not permitted",
"read-only file system",
"no space left on device",
"resource temporarily unavailable",
"device or resource busy",
"connection refused",
"connection reset",
"connection timed out",
"network is unreachable",
"host is unreachable",
"no route to host",
"could not resolve host",
"name or service not known",
"temporary failure in name resolution",
"address already in use",
"timed out",
"timeout",
"broken pipe",
"could not connect",
"service unavailable",
"is not running",
"communications error",
];
pub fn classify(snippet: &str) -> BashFailureKind {
let scanned = stderr_section(snippet)
.unwrap_or(snippet)
.to_ascii_lowercase();
if MODEL_ERROR_MARKERS.iter().any(|m| scanned.contains(m)) {
return BashFailureKind::ModelError;
}
if ENVIRONMENTAL_MARKERS.iter().any(|m| scanned.contains(m)) {
return BashFailureKind::Environmental;
}
BashFailureKind::Unknown
}
pub fn exit_code(snippet: &str) -> Option<i32> {
let rest = snippet.split_once("exited with code ")?.1;
let digits: String = rest
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '-')
.collect();
digits.parse().ok()
}
pub fn stderr_head(snippet: &str, cap: usize) -> Option<&str> {
let stderr = stderr_section(snippet)?.trim();
if stderr.is_empty() {
return None;
}
Some(match stderr.char_indices().nth(cap) {
Some((end, _)) => &stderr[..end],
None => stderr,
})
}
fn stderr_section(snippet: &str) -> Option<&str> {
snippet.split_once("STDERR:\n").map(|(_, tail)| tail)
}