use std::process::Command;
#[derive(Debug, thiserror::Error)]
#[allow(clippy::exhaustive_enums)] pub enum CmdError {
#[error("command not found: {0}")]
NotFound(String),
#[error("command failed with exit code {code}: {stderr}")]
Failed {
code: i32,
stderr: String,
},
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
pub fn run_cmd_result(cmd: &str) -> std::result::Result<String, CmdError> {
let output = Command::new("sh").arg("-c").arg(cmd).output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let code = output.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(CmdError::Failed { code, stderr })
}
}
pub fn run_cmd(cmd: &str) -> std::result::Result<String, CmdError> {
run_cmd_result(cmd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_cmd_echo() {
let result = run_cmd("echo hello").unwrap();
assert_eq!(result, "hello");
}
#[test]
fn test_run_cmd_echo_with_spaces() {
let result = run_cmd("echo 'hello world'").unwrap();
assert!(result.contains("hello world"), "Got: {}", result);
}
#[test]
fn test_run_cmd_empty_string() {
let result = run_cmd("").unwrap();
assert_eq!(result, "", "Empty command should produce empty output");
}
#[test]
fn test_run_cmd_nonexistent_command() {
let result = run_cmd("nonexistent_command_xyz_123");
assert!(result.is_err(), "Nonexistent command should return Err");
let err = result.unwrap_err();
assert!(matches!(err, CmdError::Failed { .. }));
}
#[test]
fn test_run_cmd_exit_nonzero() {
let result = run_cmd("exit 1");
assert!(result.is_err(), "Non-zero exit should return Err");
match result.unwrap_err() {
CmdError::Failed { code, stderr: _ } => assert_eq!(code, 1),
other => panic!("Expected CmdError::Failed, got: {:?}", other),
}
}
#[test]
fn test_run_cmd_returns_trimmed_output() {
let result = run_cmd("echo ' spaces '").unwrap();
assert_eq!(result, "spaces");
}
#[test]
fn test_cmd_error_display() {
let err = CmdError::NotFound("gcc".into());
assert!(err.to_string().contains("command not found"));
let err = CmdError::Failed {
code: 2,
stderr: "no input files".into(),
};
let msg = err.to_string();
assert!(msg.contains("exit code 2"));
assert!(msg.contains("no input files"));
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let err: CmdError = io_err.into();
assert!(matches!(err, CmdError::Io(_)));
assert!(err.to_string().contains("io error"));
}
#[test]
fn test_cmd_error_debug_format() {
let err = CmdError::NotFound("test".into());
let debug = format!("{:?}", err);
assert!(debug.contains("NotFound"));
let err = CmdError::Failed {
code: 1,
stderr: "err".into(),
};
let debug = format!("{:?}", err);
assert!(debug.contains("Failed"));
}
}