1use std::error::Error;
2use std::fmt;
3use std::fs::{self, File};
4use std::io;
5use std::io::{BufRead, BufReader, Write};
6use std::path::{Path, PathBuf};
7use std::process::{Child, Command, Output, Stdio};
8use std::thread;
9
10use sal_text;
11
12#[derive(Debug)]
14pub enum RunError {
15 EmptyCommand,
17 CommandExecutionFailed(io::Error),
19 CommandFailed(String),
21 ScriptPreparationFailed(String),
23 ChildProcessError(String),
25 TempDirCreationFailed(io::Error),
27 FileCreationFailed(io::Error),
29 FileWriteFailed(io::Error),
31 PermissionError(io::Error),
33}
34
35impl fmt::Display for RunError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 RunError::EmptyCommand => write!(f, "Empty command"),
40 RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
41 RunError::CommandFailed(e) => write!(f, "{}", e),
42 RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
43 RunError::ChildProcessError(e) => write!(f, "{}", e),
44 RunError::TempDirCreationFailed(e) => {
45 write!(f, "Failed to create temporary directory: {}", e)
46 }
47 RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
48 RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e),
49 RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
50 }
51 }
52}
53
54impl Error for RunError {
56 fn source(&self) -> Option<&(dyn Error + 'static)> {
57 match self {
58 RunError::CommandExecutionFailed(e) => Some(e),
59 RunError::TempDirCreationFailed(e) => Some(e),
60 RunError::FileCreationFailed(e) => Some(e),
61 RunError::FileWriteFailed(e) => Some(e),
62 RunError::PermissionError(e) => Some(e),
63 _ => None,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct CommandResult {
71 pub stdout: String,
72 pub stderr: String,
73 pub success: bool,
74 pub code: i32,
75}
76
77impl CommandResult {
78 }
80
81fn prepare_script_file(
83 script_content: &str,
84) -> Result<(PathBuf, String, tempfile::TempDir), RunError> {
85 let dedented = sal_text::dedent(script_content);
87
88 let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?;
90
91 #[cfg(target_os = "windows")]
93 let (ext, interpreter) = (".bat", "cmd.exe".to_string());
94
95 #[cfg(any(target_os = "macos", target_os = "linux"))]
96 let (ext, interpreter) = (".sh", "/bin/bash".to_string());
97
98 let script_path = temp_dir.path().join(format!("script{}", ext));
100 let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?;
101
102 #[cfg(any(target_os = "macos", target_os = "linux"))]
104 {
105 let script_with_shebang = if dedented.trim_start().starts_with("#!") {
106 dedented
108 } else {
109 format!("#!/bin/bash -e\n{}", dedented)
111 };
112
113 file.write_all(script_with_shebang.as_bytes())
115 .map_err(RunError::FileWriteFailed)?;
116 }
117
118 #[cfg(target_os = "windows")]
120 {
121 file.write_all(dedented.as_bytes())
122 .map_err(RunError::FileWriteFailed)?;
123 }
124
125 #[cfg(any(target_os = "macos", target_os = "linux"))]
127 {
128 use std::os::unix::fs::PermissionsExt;
129 let mut perms = fs::metadata(&script_path)
130 .map_err(|e| RunError::PermissionError(e))?
131 .permissions();
132 perms.set_mode(0o755); fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
134 }
135
136 Ok((script_path, interpreter, temp_dir))
137}
138
139fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
141 let stdout = child.stdout.take();
143 let stderr = child.stderr.take();
144
145 let stdout_handle = if let Some(out) = stdout {
147 let reader = BufReader::new(out);
148 let silent_clone = silent;
149 Some(std::thread::spawn(move || {
151 let mut local_buffer = String::new();
152 for line in reader.lines() {
153 if let Ok(l) = line {
154 if !silent_clone {
156 println!("{}", l);
157 std::io::stdout().flush().unwrap_or(());
158 }
159 local_buffer.push_str(&l);
161 local_buffer.push('\n');
162 }
163 }
164 local_buffer
165 }))
166 } else {
167 None
168 };
169
170 let stderr_handle = if let Some(err) = stderr {
172 let reader = BufReader::new(err);
173 let silent_clone = silent;
174 Some(std::thread::spawn(move || {
176 let mut local_buffer = String::new();
177 for line in reader.lines() {
178 if let Ok(l) = line {
179 if !silent_clone {
181 eprintln!("\x1b[31mERROR: {}\x1b[0m", l); std::io::stderr().flush().unwrap_or(());
184 }
185 local_buffer.push_str(&l);
187 local_buffer.push('\n');
188 }
189 }
190 local_buffer
191 }))
192 } else {
193 None
194 };
195
196 let status = child.wait().map_err(|e| {
198 RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
199 })?;
200
201 let captured_stdout = if let Some(handle) = stdout_handle {
203 handle.join().unwrap_or_default()
204 } else {
205 "Failed to capture stdout".to_string()
206 };
207
208 let captured_stderr = if let Some(handle) = stderr_handle {
210 handle.join().unwrap_or_default()
211 } else {
212 "Failed to capture stderr".to_string()
213 };
214
215 if !status.success() && silent && !captured_stderr.is_empty() {
217 eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
218 for line in captured_stderr.lines() {
219 eprintln!("\x1b[31m{}\x1b[0m", line);
220 }
221 }
222
223 Ok(CommandResult {
225 stdout: captured_stdout,
226 stderr: captured_stderr,
227 success: status.success(),
228 code: status.code().unwrap_or(-1),
229 })
230}
231
232fn process_command_output(
234 output: Result<Output, std::io::Error>,
235) -> Result<CommandResult, RunError> {
236 match output {
237 Ok(out) => {
238 let stdout = String::from_utf8_lossy(&out.stdout).to_string();
239 let stderr = String::from_utf8_lossy(&out.stderr).to_string();
240 if !out.status.success() {
245 eprintln!(
246 "\x1b[31mCommand failed with exit code: {}\x1b[0m",
247 out.status.code().unwrap_or(-1)
248 );
249 }
250
251 Ok(CommandResult {
252 stdout,
253 stderr,
254 success: out.status.success(),
255 code: out.status.code().unwrap_or(-1),
256 })
257 }
258 Err(e) => Err(RunError::CommandExecutionFailed(e)),
259 }
260}
261
262fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
264 let mut parts = command.split_whitespace();
265 let cmd = match parts.next() {
266 Some(c) => c,
267 None => return Err(RunError::EmptyCommand),
268 };
269
270 let args: Vec<&str> = parts.collect();
271
272 let child = Command::new(cmd)
274 .args(&args)
275 .stdout(Stdio::piped())
276 .stderr(Stdio::piped())
277 .spawn()
278 .map_err(RunError::CommandExecutionFailed)?;
279
280 handle_child_output(child, silent)
281}
282
283fn execute_script_internal(
285 interpreter: &str,
286 script_path: &Path,
287 silent: bool,
288) -> Result<CommandResult, RunError> {
289 #[cfg(target_os = "windows")]
290 let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
291
292 #[cfg(any(target_os = "macos", target_os = "linux"))]
293 let command_args = vec!["-e", script_path.to_str().unwrap_or("")];
294
295 if silent {
296 let output = Command::new(interpreter).args(&command_args).output();
298
299 let result = process_command_output(output)?;
300
301 if !result.success {
303 return Err(RunError::CommandFailed(format!(
304 "Script execution failed with exit code {}: {}",
305 result.code,
306 result.stderr.trim()
307 )));
308 }
309
310 Ok(result)
311 } else {
312 let child = Command::new(interpreter)
314 .args(&command_args)
315 .stdout(Stdio::piped())
316 .stderr(Stdio::piped())
317 .spawn()
318 .map_err(RunError::CommandExecutionFailed)?;
319
320 let result = handle_child_output(child, false)?;
321
322 if !result.success {
324 return Err(RunError::CommandFailed(format!(
325 "Script execution failed with exit code {}: {}",
326 result.code,
327 result.stderr.trim()
328 )));
329 }
330
331 Ok(result)
332 }
333}
334
335fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
337 let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
339
340 if !silent {
342 println!("\x1b[36mExecuting script:\x1b[0m");
343
344 if let Ok(script_content) = fs::read_to_string(&script_path) {
346 for (i, line) in script_content.lines().enumerate() {
347 println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
348 }
349 } else {
350 for (i, line) in script.lines().enumerate() {
352 println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
353 }
354 }
355
356 println!("\x1b[36m---\x1b[0m");
357 }
358
359 let result = execute_script_internal(&interpreter, &script_path, silent);
364
365 if let Err(ref e) = result {
368 if !matches!(e, RunError::CommandFailed(_)) {
369 eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
370 }
371 }
372
373 result
374}
375
376pub struct RunBuilder<'a> {
378 cmd: &'a str,
380 die: bool,
382 silent: bool,
384 async_exec: bool,
386 log: bool,
388}
389
390impl<'a> RunBuilder<'a> {
391 pub fn new(cmd: &'a str) -> Self {
393 Self {
394 cmd,
395 die: true,
396 silent: false,
397 async_exec: false,
398 log: false,
399 }
400 }
401
402 pub fn die(mut self, die: bool) -> Self {
404 self.die = die;
405 self
406 }
407
408 pub fn silent(mut self, silent: bool) -> Self {
410 self.silent = silent;
411 self
412 }
413
414 pub fn async_exec(mut self, async_exec: bool) -> Self {
416 self.async_exec = async_exec;
417 self
418 }
419
420 pub fn log(mut self, log: bool) -> Self {
422 self.log = log;
423 self
424 }
425
426 pub fn execute(self) -> Result<CommandResult, RunError> {
428 let trimmed = self.cmd.trim();
429
430 if self.log {
432 println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
433 }
434
435 if self.async_exec {
437 let cmd_copy = trimmed.to_string();
438 let silent = self.silent;
439 let log = self.log;
440
441 thread::spawn(move || {
443 if log {
444 println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
445 }
446
447 let result = if cmd_copy.contains('\n') {
448 run_script_internal(&cmd_copy, silent)
449 } else {
450 run_command_internal(&cmd_copy, silent)
451 };
452
453 if log {
454 match &result {
455 Ok(res) => {
456 if res.success {
457 println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
458 } else {
459 eprintln!(
460 "\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m",
461 res.code
462 );
463 }
464 }
465 Err(e) => {
466 eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
467 }
468 }
469 }
470 });
471
472 return Ok(CommandResult {
474 stdout: String::new(),
475 stderr: String::new(),
476 success: true,
477 code: 0,
478 });
479 }
480
481 let result = if trimmed.contains('\n') {
483 run_script_internal(trimmed, self.silent)
485 } else {
486 run_command_internal(trimmed, self.silent)
488 };
489
490 match result {
492 Ok(res) => {
493 if !res.success && !self.die && !self.silent {
495 eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
496 }
497 Ok(res)
498 }
499 Err(e) => {
500 if !matches!(e, RunError::CommandFailed(_)) {
503 eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
504 }
505
506 if self.die {
507 Err(e)
508 } else {
509 Ok(CommandResult {
511 stdout: String::new(),
512 stderr: format!("Error: {}", e),
513 success: false,
514 code: -1,
515 })
516 }
517 }
518 }
519 }
520}
521
522pub fn run(cmd: &str) -> RunBuilder {
524 RunBuilder::new(cmd)
525}
526
527pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
529 run(command).execute()
530}
531
532pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
534 run(command).silent(true).execute()
535}