use std::io::{BufRead, BufReader, Write}; use std::fs::{self, File}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output, Stdio}; use std::fmt; use std::error::Error; use std::io; use std::thread; use crate::text; /// Error type for command and script execution operations #[derive(Debug)] pub enum RunError { /// The command string was empty EmptyCommand, /// An error occurred while executing a command CommandExecutionFailed(io::Error), /// A command executed successfully but returned an error CommandFailed(String), /// An error occurred while preparing a script for execution ScriptPreparationFailed(String), /// An error occurred in a child process ChildProcessError(String), /// Failed to create a temporary directory TempDirCreationFailed(io::Error), /// Failed to create a script file FileCreationFailed(io::Error), /// Failed to write to a script file FileWriteFailed(io::Error), /// Failed to set file permissions PermissionError(io::Error), } /// Implement Display for RunError to provide human-readable error messages impl fmt::Display for RunError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RunError::EmptyCommand => write!(f, "Empty command"), RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e), RunError::CommandFailed(e) => write!(f, "{}", e), RunError::ScriptPreparationFailed(e) => write!(f, "{}", e), RunError::ChildProcessError(e) => write!(f, "{}", e), RunError::TempDirCreationFailed(e) => write!(f, "Failed to create temporary directory: {}", e), RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e), RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e), RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e), } } } // Implement Error trait for RunError impl Error for RunError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { RunError::CommandExecutionFailed(e) => Some(e), RunError::TempDirCreationFailed(e) => Some(e), RunError::FileCreationFailed(e) => Some(e), RunError::FileWriteFailed(e) => Some(e), RunError::PermissionError(e) => Some(e), _ => None, } } } /// A structure to hold command execution results #[derive(Debug, Clone)] pub struct CommandResult { pub stdout: String, pub stderr: String, pub success: bool, pub code: i32, } impl CommandResult { /// Create a default failed result with an error message fn _error(message: &str) -> Self { Self { stdout: String::new(), stderr: message.to_string(), success: false, code: -1, } } } /// Prepare a script file and return the path and interpreter fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfile::TempDir), RunError> { // Dedent the script let dedented = text::dedent(script_content); // Create a temporary directory let temp_dir = tempfile::tempdir() .map_err(RunError::TempDirCreationFailed)?; // Determine script extension and interpreter #[cfg(target_os = "windows")] let (ext, interpreter) = (".bat", "cmd.exe".to_string()); #[cfg(any(target_os = "macos", target_os = "linux"))] let (ext, interpreter) = (".sh", "/bin/bash".to_string()); // Create the script file let script_path = temp_dir.path().join(format!("script{}", ext)); let mut file = File::create(&script_path) .map_err(RunError::FileCreationFailed)?; // For Unix systems, ensure the script has a shebang line with -e flag #[cfg(any(target_os = "macos", target_os = "linux"))] { let script_with_shebang = if dedented.trim_start().starts_with("#!") { // Script already has a shebang, use it as is dedented } else { // Add shebang with -e flag to ensure script fails on errors format!("#!/bin/bash -e\n{}", dedented) }; // Write the script content with shebang file.write_all(script_with_shebang.as_bytes()) .map_err(RunError::FileWriteFailed)?; } // For Windows, just write the script as is #[cfg(target_os = "windows")] { file.write_all(dedented.as_bytes()) .map_err(RunError::FileWriteFailed)?; } // Make the script executable (Unix only) #[cfg(any(target_os = "macos", target_os = "linux"))] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&script_path) .map_err(|e| RunError::PermissionError(e))? .permissions(); perms.set_mode(0o755); // rwxr-xr-x fs::set_permissions(&script_path, perms) .map_err(RunError::PermissionError)?; } Ok((script_path, interpreter, temp_dir)) } /// Capture output from Child's stdio streams with optional printing fn handle_child_output(mut child: Child, silent: bool) -> Result { // Prepare to read stdout & stderr line-by-line let stdout = child.stdout.take(); let stderr = child.stderr.take(); // Process stdout let stdout_handle = if let Some(out) = stdout { let reader = BufReader::new(out); let silent_clone = silent; // Spawn a thread to capture and optionally print stdout Some(std::thread::spawn(move || { let mut local_buffer = String::new(); for line in reader.lines() { if let Ok(l) = line { // Print the line if not silent and flush immediately if !silent_clone { println!("{}", l); std::io::stdout().flush().unwrap_or(()); } // Store it in our captured buffer local_buffer.push_str(&l); local_buffer.push('\n'); } } local_buffer })) } else { None }; // Process stderr let stderr_handle = if let Some(err) = stderr { let reader = BufReader::new(err); let silent_clone = silent; // Spawn a thread to capture and optionally print stderr Some(std::thread::spawn(move || { let mut local_buffer = String::new(); for line in reader.lines() { if let Ok(l) = line { // Print the line if not silent and flush immediately if !silent_clone { // Always print stderr, even if silent is true, for error visibility eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors std::io::stderr().flush().unwrap_or(()); } // Store it in our captured buffer local_buffer.push_str(&l); local_buffer.push('\n'); } } local_buffer })) } else { None }; // Wait for the child process to exit let status = child.wait() .map_err(|e| RunError::ChildProcessError(format!("Failed to wait on child process: {}", e)))?; // Join our stdout thread if it exists let captured_stdout = if let Some(handle) = stdout_handle { handle.join().unwrap_or_default() } else { "Failed to capture stdout".to_string() }; // Join our stderr thread if it exists let captured_stderr = if let Some(handle) = stderr_handle { handle.join().unwrap_or_default() } else { "Failed to capture stderr".to_string() }; // If the command failed, print the stderr if it wasn't already printed if !status.success() && silent && !captured_stderr.is_empty() { eprintln!("\x1b[31mCommand failed with error:\x1b[0m"); for line in captured_stderr.lines() { eprintln!("\x1b[31m{}\x1b[0m", line); } } // Return the command result Ok(CommandResult { stdout: captured_stdout, stderr: captured_stderr, success: status.success(), code: status.code().unwrap_or(-1), }) } /// Processes Output structure from Command::output() into CommandResult fn process_command_output(output: Result) -> Result { match output { Ok(out) => { let stdout = String::from_utf8_lossy(&out.stdout).to_string(); let stderr = String::from_utf8_lossy(&out.stderr).to_string(); // Print stderr if there's any, even for silent execution if !stderr.is_empty() { eprintln!("\x1b[31mCommand stderr output:\x1b[0m"); for line in stderr.lines() { eprintln!("\x1b[31m{}\x1b[0m", line); } } // If the command failed, print a clear error message if !out.status.success() { eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m", out.status.code().unwrap_or(-1)); } Ok(CommandResult { stdout, stderr, success: out.status.success(), code: out.status.code().unwrap_or(-1), }) }, Err(e) => Err(RunError::CommandExecutionFailed(e)), } } /// Common logic for running a command with optional silent mode fn run_command_internal(command: &str, silent: bool) -> Result { let mut parts = command.split_whitespace(); let cmd = match parts.next() { Some(c) => c, None => return Err(RunError::EmptyCommand), }; let args: Vec<&str> = parts.collect(); // Spawn the child process with piped stdout & stderr let child = Command::new(cmd) .args(&args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(RunError::CommandExecutionFailed)?; handle_child_output(child, silent) } /// Execute a script with the given interpreter and path fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result { #[cfg(target_os = "windows")] let command_args = vec!["/c", script_path.to_str().unwrap_or("")]; #[cfg(any(target_os = "macos", target_os = "linux"))] let command_args = vec![script_path.to_str().unwrap_or("")]; if silent { // For silent execution, use output() which captures but doesn't display let output = Command::new(interpreter) .args(&command_args) .output(); let result = process_command_output(output)?; // If the script failed, return an error if !result.success { return Err(RunError::CommandFailed(format!( "Script execution failed with exit code {}: {}", result.code, result.stderr.trim() ))); } Ok(result) } else { // For normal execution, spawn and handle the output streams let child = Command::new(interpreter) .args(&command_args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(RunError::CommandExecutionFailed)?; let result = handle_child_output(child, false)?; // If the script failed, return an error if !result.success { return Err(RunError::CommandFailed(format!( "Script execution failed with exit code {}: {}", result.code, result.stderr.trim() ))); } Ok(result) } } /// Run a multiline script with optional silent mode fn run_script_internal(script: &str, silent: bool) -> Result { // Print the script being executed if not silent if !silent { println!("\x1b[36mExecuting script:\x1b[0m"); for (i, line) in script.lines().enumerate() { println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line); } println!("\x1b[36m---\x1b[0m"); } let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?; // _temp_dir is kept in scope until the end of this function to ensure // it's not dropped prematurely, which would clean up the directory // Execute the script and handle the result let result = execute_script_internal(&interpreter, &script_path, silent); // If there was an error, print a clear error message if let Err(ref e) = result { eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e); } result } /// A builder for configuring and executing commands or scripts pub struct RunBuilder<'a> { /// The command or script to run cmd: &'a str, /// Whether to return an error if the command fails (default: true) die: bool, /// Whether to suppress output to stdout/stderr (default: false) silent: bool, /// Whether to run the command asynchronously (default: false) async_exec: bool, /// Whether to log command execution (default: false) log: bool, } impl<'a> RunBuilder<'a> { /// Create a new RunBuilder with default settings pub fn new(cmd: &'a str) -> Self { Self { cmd, die: true, silent: false, async_exec: false, log: false, } } /// Set whether to return an error if the command fails pub fn die(mut self, die: bool) -> Self { self.die = die; self } /// Set whether to suppress output to stdout/stderr pub fn silent(mut self, silent: bool) -> Self { self.silent = silent; self } /// Set whether to run the command asynchronously pub fn async_exec(mut self, async_exec: bool) -> Self { self.async_exec = async_exec; self } /// Set whether to log command execution pub fn log(mut self, log: bool) -> Self { self.log = log; self } /// Execute the command or script with the configured options pub fn execute(self) -> Result { let trimmed = self.cmd.trim(); // Log command execution if enabled if self.log { println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed); } // Handle async execution if self.async_exec { let cmd_copy = trimmed.to_string(); let silent = self.silent; let log = self.log; // Spawn a thread to run the command asynchronously thread::spawn(move || { if log { println!("\x1b[36m[ASYNC] Starting execution\x1b[0m"); } let result = if cmd_copy.contains('\n') { run_script_internal(&cmd_copy, silent) } else { run_command_internal(&cmd_copy, silent) }; if log { match &result { Ok(res) => { if res.success { println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m"); } else { eprintln!("\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", res.code); } }, Err(e) => { eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e); } } } }); // Return a placeholder result for async execution return Ok(CommandResult { stdout: String::new(), stderr: String::new(), success: true, code: 0, }); } // Execute the command or script let result = if trimmed.contains('\n') { // This is a multiline script run_script_internal(trimmed, self.silent) } else { // This is a single command run_command_internal(trimmed, self.silent) }; // Handle die=false: convert errors to CommandResult with success=false match result { Ok(res) => { // If the command failed but die is false, print a warning if !res.success && !self.die && !self.silent { eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code); } Ok(res) }, Err(e) => { // Always print the error, even if die is false eprintln!("\x1b[31mCommand error: {}\x1b[0m", e); if self.die { Err(e) } else { // Convert error to CommandResult with success=false Ok(CommandResult { stdout: String::new(), stderr: format!("Error: {}", e), success: false, code: -1, }) } } } } } /// Create a new RunBuilder for executing a command or script pub fn run(cmd: &str) -> RunBuilder { RunBuilder::new(cmd) } /// Run a command or multiline script with arguments pub fn run_command(command: &str) -> Result { run(command).execute() } /// Run a command or multiline script with arguments silently pub fn run_silent(command: &str) -> Result { run(command).silent(true).execute() }