This commit is contained in:
2025-04-05 05:46:30 +02:00
parent e48063a79c
commit 7cdd9f5559
52 changed files with 2062 additions and 1256 deletions

View File

@@ -98,16 +98,35 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
#[cfg(any(target_os = "macos", target_os = "linux"))]
let (ext, interpreter) = (".sh", "/bin/sh".to_string());
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)?;
// Write the script content
file.write_all(dedented.as_bytes())
.map_err(RunError::FileWriteFailed)?;
// 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"))]
@@ -166,7 +185,8 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
if let Ok(l) = line {
// Print the line if not silent and flush immediately
if !silent_clone {
eprintln!("{}", l);
// 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
@@ -198,6 +218,14 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
"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,
@@ -214,6 +242,20 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
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,
@@ -260,7 +302,18 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.args(&command_args)
.output();
process_command_output(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)
@@ -270,16 +323,45 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
.spawn()
.map_err(RunError::CommandExecutionFailed)?;
handle_child_output(child, false)
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<CommandResult, RunError> {
// 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_script_internal(&interpreter, &script_path, silent)
// 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
@@ -338,21 +420,41 @@ impl<'a> RunBuilder<'a> {
// Log command execution if enabled
if self.log {
println!("[LOG] Executing command: {}", trimmed);
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 || {
let _ = if cmd_copy.contains('\n') {
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
@@ -375,8 +477,17 @@ impl<'a> RunBuilder<'a> {
// Handle die=false: convert errors to CommandResult with success=false
match result {
Ok(res) => Ok(res),
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 {