volition_core/tools/
shell.rs1use super::CommandOutput;
6use anyhow::{Context, Result};
7use std::path::Path;
8use std::process::{Command, Stdio};
9use tracing::{debug, warn};
10
11pub async fn execute_shell_command(command: &str, working_dir: &Path) -> Result<CommandOutput> {
33 debug!("Executing shell command: {} in {:?}", command, working_dir);
34
35 let shell_executable = if cfg!(target_os = "windows") {
36 "cmd"
37 } else {
38 "sh"
39 };
40 let shell_arg = if cfg!(target_os = "windows") {
41 "/C"
42 } else {
43 "-c"
44 };
45
46 let output_result = Command::new(shell_executable)
47 .current_dir(working_dir)
48 .arg(shell_arg)
49 .arg(command)
50 .stdout(Stdio::piped())
51 .stderr(Stdio::piped())
52 .output()
53 .with_context(|| format!("Failed to spawn shell process for command: {}", command));
54
55 let output = match output_result {
56 Ok(out) => out,
57 Err(e) => {
58 warn!(command = command, error = %e, "Failed to spawn command process");
59 return Err(e);
60 }
61 };
62
63 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
64 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
65 let status = output.status.code().unwrap_or(-1);
66
67 debug!(
68 "Shell command exit status: {}\nStdout preview (first 3 lines):\n{}\nStderr preview (first 3 lines):\n{}",
69 status,
70 stdout.lines().take(3).collect::<Vec<_>>().join("\n"),
71 stderr.lines().take(3).collect::<Vec<_>>().join("\n")
72 );
73
74 Ok(CommandOutput {
75 status,
76 stdout,
77 stderr,
78 })
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::path::PathBuf;
85 use tempfile::tempdir;
86 use tokio;
87
88 fn test_working_dir() -> PathBuf {
89 tempdir().map(|d| d.into_path()).unwrap_or_default()
90 }
91
92 #[tokio::test]
93 async fn test_execute_shell_echo() {
94 let command = "echo Hello Core Shell";
95 let working_dir = test_working_dir();
96 let result = execute_shell_command(command, &working_dir).await;
97 assert!(result.is_ok(), "Command failed: {:?}", result.err());
98 let output = result.unwrap();
99 println!("Output: {:?}", output);
100 assert_eq!(output.status, 0);
101 assert_eq!(output.stdout.trim(), "Hello Core Shell");
102 assert!(output.stderr.is_empty() || output.stderr == "<no output>");
103 }
104
105 #[tokio::test]
106 async fn test_execute_shell_nonexistent_command() {
107 let command = "this_command_does_not_exist_qwertyuiop";
108 let working_dir = test_working_dir();
109 let result = execute_shell_command(command, &working_dir).await;
110 assert!(result.is_ok());
111 let output = result.unwrap();
112 println!("Output: {:?}", output);
113 assert_ne!(output.status, 0);
114 assert!(output.stdout.is_empty() || output.stdout == "<no output>");
115 assert!(output.stderr.contains("not found") || output.stderr.contains("is not recognized"));
116 }
117}