This commit is contained in:
2025-04-04 15:05:48 +02:00
parent dc49e78d00
commit eecbed4b1f
7 changed files with 1860 additions and 224 deletions

View File

@@ -5,13 +5,11 @@ 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
///
/// This enum represents various errors that can occur during command and script
/// execution, including preparation, execution, and output handling.
#[derive(Debug)]
pub enum RunError {
/// The command string was empty
@@ -227,19 +225,7 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
}
}
/**
* Common logic for running a command with optional silent mode.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*/
/// Common logic for running a command with optional silent mode
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
let mut parts = command.split_whitespace();
let cmd = match parts.next() {
@@ -260,20 +246,7 @@ fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, Ru
handle_child_output(child, silent)
}
/**
* Execute a script with the given interpreter and path.
*
* # Arguments
*
* * `interpreter` - The interpreter to use (e.g., "/bin/sh")
* * `script_path` - The path to the script file
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*/
/// Execute a script with the given interpreter and path
fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result<CommandResult, RunError> {
#[cfg(target_os = "windows")]
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
@@ -301,65 +274,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
}
}
/**
* Run a single command with arguments, showing live stdout and stderr.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*
* # Examples
*
* ```
* let result = run_command("ls -la")?;
* println!("Command exited with code: {}", result.code);
* ```
*/
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
run_command_internal(command, /* silent = */ false)
}
/**
* Run a single command with arguments silently.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*
* # Examples
*
* ```
* let result = run_command_silent("ls -la")?;
* println!("Command output: {}", result.stdout);
* ```
*/
pub fn run_command_silent(command: &str) -> Result<CommandResult, RunError> {
run_command_internal(command, /* silent = */ true)
}
/**
* Run a multiline script with optional silent mode.
*
* # Arguments
*
* * `script` - The script content as a string
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*/
/// Run a multiline script with optional silent mode
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
// _temp_dir is kept in scope until the end of this function to ensure
@@ -367,134 +282,128 @@ fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunE
execute_script_internal(&interpreter, &script_path, silent)
}
/**
* Run a multiline script by saving it to a temporary file and executing.
*
* # Arguments
*
* * `script` - The script content as a string
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*
* # Examples
*
* ```
* let script = r#"
* echo "Hello, world!"
* ls -la
* "#;
* let result = run_script(script)?;
* println!("Script exited with code: {}", result.code);
* ```
*/
pub fn run_script(script: &str) -> Result<CommandResult, RunError> {
run_script_internal(script, false)
/// 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,
}
/**
* Run a multiline script silently by saving it to a temporary file and executing.
*
* # Arguments
*
* * `script` - The script content as a string
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*
* # Examples
*
* ```
* let script = r#"
* echo "Hello, world!"
* ls -la
* "#;
* let result = run_script_silent(script)?;
* println!("Script output: {}", result.stdout);
* ```
*/
pub fn run_script_silent(script: &str) -> Result<CommandResult, RunError> {
run_script_internal(script, true)
}
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,
}
}
/**
* Run a command or multiline script with arguments.
* Shows stdout/stderr as it arrives.
*
* # Arguments
*
* * `command` - The command or script to run
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the execution
* * `Err(RunError)` - An error if the execution failed
*
* # Examples
*
* ```
* // Run a single command
* let result = run("ls -la")?;
*
* // Run a multiline script
* let result = run(r#"
* echo "Hello, world!"
* ls -la
* "#)?;
* ```
*/
pub fn run(command: &str) -> Result<CommandResult, RunError> {
let trimmed = command.trim();
// Check if this is a multiline script
if trimmed.contains('\n') {
// This is a multiline script, write to a temporary file and execute
run_script(trimmed)
} else {
// This is a single command with arguments
run_command(trimmed)
/// 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<CommandResult, RunError> {
let trimmed = self.cmd.trim();
// Log command execution if enabled
if self.log {
println!("[LOG] Executing command: {}", trimmed);
}
// Handle async execution
if self.async_exec {
let cmd_copy = trimmed.to_string();
let silent = self.silent;
// Spawn a thread to run the command asynchronously
thread::spawn(move || {
let _ = if cmd_copy.contains('\n') {
run_script_internal(&cmd_copy, silent)
} else {
run_command_internal(&cmd_copy, silent)
};
});
// 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) => Ok(res),
Err(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,
})
}
}
}
}
}
/**
* Run a command or multiline script with arguments silently.
* Doesn't show stdout/stderr as it arrives.
*
* # Arguments
*
* * `command` - The command or script to run
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the execution
* * `Err(RunError)` - An error if the execution failed
*
* # Examples
*
* ```
* // Run a single command silently
* let result = run_silent("ls -la")?;
*
* // Run a multiline script silently
* let result = run_silent(r#"
* echo "Hello, world!"
* ls -la
* "#)?;
* ```
*/
/// 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<CommandResult, RunError> {
run(command).execute()
}
/// Run a command or multiline script with arguments silently
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
let trimmed = command.trim();
// Check if this is a multiline script
if trimmed.contains('\n') {
// This is a multiline script, write to a temporary file and execute
run_script_silent(trimmed)
} else {
// This is a single command with arguments
run_command_silent(trimmed)
}
run(command).silent(true).execute()
}