...
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user