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()
}

View File

@@ -1,12 +1,15 @@
#[cfg(test)]
mod tests {
use crate::process::run::{run, run_silent, run_script, run_command};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use crate::process::run::{run, RunError};
use crate::text::dedent;
#[test]
fn test_run_command() {
// Test running a simple echo command
let result = run_command("echo hello").unwrap();
// Test running a simple echo command using the builder pattern
let result = run("echo hello").execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.trim().contains("hello"));
@@ -15,8 +18,8 @@ mod tests {
#[test]
fn test_run_silent_command() {
// Test running a command silently
let result = run_silent("echo silent test").unwrap();
// Test running a command silently using the builder pattern
let result = run("echo silent test").silent(true).execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.trim().contains("silent test"));
@@ -25,13 +28,13 @@ mod tests {
#[test]
fn test_run_script() {
// Test running a multi-line script
// Test running a multi-line script using the builder pattern
let script = r#"
echo "line 1"
echo "line 2"
"#;
let result = run_script(script).unwrap();
let result = run(script).execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("line 1"));
@@ -53,7 +56,7 @@ mod tests {
assert!(dedented.contains(" echo \"This has 16 spaces (4 more than the common indentation)\""));
// Running the script should work with the dedented content
let result = run(script).unwrap();
let result = run(script).execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.contains("This has 12 spaces of indentation"));
@@ -66,13 +69,13 @@ mod tests {
// One-liner should be treated as a command
let one_liner = "echo one-liner test";
let result = run(one_liner).unwrap();
let result = run(one_liner).execute().unwrap();
assert!(result.success);
assert!(result.stdout.contains("one-liner test"));
// Multi-line input should be treated as a script
let multi_line = "echo first line\necho second line";
let result = run(multi_line).unwrap();
let result = run(multi_line).execute().unwrap();
assert!(result.success);
assert!(result.stdout.contains("first line"));
assert!(result.stdout.contains("second line"));
@@ -81,12 +84,86 @@ mod tests {
#[test]
fn test_run_empty_command() {
// Test handling of empty commands
let result = run("");
let result = run("").execute();
assert!(result.is_err());
// The specific error should be EmptyCommand
match result {
Err(crate::process::run::RunError::EmptyCommand) => (),
Err(RunError::EmptyCommand) => (),
_ => panic!("Expected EmptyCommand error"),
}
}
#[test]
fn test_run_die_option() {
// Test the die option - when false, it should return a CommandResult with success=false
// instead of an Err when the command fails
// With die=true (default), a non-existent command should return an error
let result = run("non_existent_command").execute();
assert!(result.is_err());
// With die=false, it should return a CommandResult with success=false
let result = run("non_existent_command").die(false).execute().unwrap();
assert!(!result.success);
assert_ne!(result.code, 0);
assert!(result.stderr.contains("Error:"));
}
#[test]
fn test_run_async_option() {
// Test the async option - when true, it should spawn the process and return immediately
// Create a shared variable to track if the command has completed
let completed = Arc::new(Mutex::new(false));
let completed_clone = completed.clone();
// Run a command that sleeps for 2 seconds, with async=true
let start = std::time::Instant::now();
let result = run("sleep 2").async_exec(true).execute().unwrap();
let elapsed = start.elapsed();
// The command should return immediately (much less than 2 seconds)
assert!(elapsed < Duration::from_secs(1));
// The result should have empty stdout/stderr and success=true
assert!(result.success);
assert_eq!(result.code, 0);
assert_eq!(result.stdout, "");
assert_eq!(result.stderr, "");
// Wait a bit to ensure the command has time to complete
thread::sleep(Duration::from_secs(3));
// Verify the command completed (this is just a placeholder since we can't easily
// check if the async command completed in this test framework)
*completed_clone.lock().unwrap() = true;
assert!(*completed.lock().unwrap());
}
#[test]
fn test_run_log_option() {
// Test the log option - when true, it should log command execution
// Note: We can't easily capture stdout in tests, so this is more of a smoke test
// Run a command with log=true
let result = run("echo log test").log(true).execute().unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.trim().contains("log test"));
}
#[test]
fn test_builder_method_chaining() {
// Test that all builder methods can be chained together
let result = run("echo chaining test")
.silent(true)
.die(true)
.log(true)
.execute()
.unwrap();
assert!(result.success);
assert_eq!(result.code, 0);
assert!(result.stdout.trim().contains("chaining test"));
}
}