1use colored::*;
2use shells::sh;
3use std::env;
4use std::fmt;
5
6#[derive(Debug)]
8pub enum ShellExecError {
9 ExecutionFailed {
10 command: String,
11 exit_code: i32,
12 stderr: Option<String>,
13 stdout: Option<String>,
14 error_id: String,
15 },
16 EnvVarNotFound {
17 var_name: String,
18 error_id: String,
19 source: env::VarError,
20 },
21 Timeout {
22 command: String,
23 duration_ms: u64,
24 error_id: String,
25 },
26 JoinFailed {
27 command: String,
28 error_id: String,
29 },
30}
31
32impl fmt::Display for ShellExecError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(f, "{}", self.format_detailed())
36 }
37}
38
39impl std::error::Error for ShellExecError {
41 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
42 match self {
43 ShellExecError::EnvVarNotFound { source, .. } => Some(source),
44 _ => None,
45 }
46 }
47}
48
49impl ShellExecError {
50 pub fn format_detailed(&self) -> String {
52 match self {
53 ShellExecError::ExecutionFailed {
54 command,
55 exit_code,
56 stderr,
57 stdout,
58 error_id,
59 } => {
60 let mut output = String::new();
61 output.push_str(&format!("{}\n", "Command Execution Failed".red().bold()));
62 output.push_str(&format!(" {}: {}\n", "Command".cyan(), command));
63 output.push_str(&format!(" {}: {}\n", "Exit Code".cyan(), exit_code));
64 output.push_str(&format!(" {}: {}\n", "Error ID".cyan(), error_id.green()));
65
66 if let Some(stdout_val) = stdout {
67 if !stdout_val.is_empty() {
68 output.push_str(&format!("\n {}:\n", "Standard Output".yellow()));
69 for line in stdout_val.lines() {
70 output.push_str(&format!(" {}\n", line));
71 }
72 }
73 }
74
75 if let Some(stderr_val) = stderr {
76 if !stderr_val.is_empty() {
77 output.push_str(&format!("\n {}:\n", "Standard Error".red()));
78 for line in stderr_val.lines() {
79 output.push_str(&format!(" {}\n", line));
80 }
81 }
82 }
83
84 output
85 }
86 ShellExecError::EnvVarNotFound {
87 var_name,
88 error_id,
89 source,
90 } => {
91 format!(
92 "{}\n {}: {}\n {}: {}\n {}: {:?}\n",
93 "Environment Variable Not Found".red().bold(),
94 "Variable".cyan(),
95 var_name,
96 "Error ID".cyan(),
97 error_id.green(),
98 "Reason".cyan(),
99 source
100 )
101 }
102 ShellExecError::Timeout {
103 command,
104 duration_ms,
105 error_id,
106 } => {
107 format!(
108 "{}\n {}: {}\n {}: {}ms\n {}: {}\n",
109 "Command Timed Out".red().bold(),
110 "Command".cyan(),
111 command,
112 "Timeout".cyan(),
113 duration_ms,
114 "Error ID".cyan(),
115 error_id.green()
116 )
117 }
118 ShellExecError::JoinFailed { command, error_id } => {
119 format!(
120 "{}\n {}: {}\n {}: {}\n",
121 "Thread Join Failed".red().bold(),
122 "Command".cyan(),
123 command,
124 "Error ID".cyan(),
125 error_id.green()
126 )
127 }
128 }
129 }
130}
131
132pub type ShellExecResult<T> = anyhow::Result<T>;
134
135#[derive(Debug, Clone)]
137pub struct CommandOutput {
138 pub stdout: String,
139 pub stderr: String,
140 pub exit_code: i32,
141}
142
143impl CommandOutput {
144 pub fn stdout(&self) -> Option<String> {
146 if self.stdout.is_empty() {
147 None
148 } else {
149 Some(self.stdout.clone())
150 }
151 }
152
153 pub fn stderr(&self) -> Option<String> {
155 if self.stderr.is_empty() {
156 None
157 } else {
158 Some(self.stderr.clone())
159 }
160 }
161
162 pub fn success(&self) -> bool {
164 self.exit_code == 0
165 }
166}
167
168pub fn execute_command(cmd: &str, error_id: &str) -> ShellExecResult<String> {
178 let output = execute_command_raw(cmd, error_id)?;
179 Ok(output.stdout)
180}
181
182pub fn execute_command_raw(cmd: &str, error_id: &str) -> Result<CommandOutput, ShellExecError> {
192 let (code, stdout, stderr) = sh!("{}", cmd);
193
194 let output = CommandOutput {
195 stdout,
196 stderr,
197 exit_code: code,
198 };
199
200 if code == 0 {
201 Ok(output)
202 } else {
203 Err(ShellExecError::ExecutionFailed {
204 command: cmd.to_string(),
205 exit_code: code,
206 stderr: output.stderr(),
207 stdout: output.stdout(),
208 error_id: error_id.to_string(),
209 }
210 .into())
211 }
212}
213
214pub fn get_env(var_name: &str, error_id: &str) -> ShellExecResult<String> {
224 env::var(var_name).map_err(|e| {
225 ShellExecError::EnvVarNotFound {
226 var_name: var_name.to_string(),
227 error_id: error_id.to_string(),
228 source: e,
229 }
230 .into()
231 })
232}
233
234pub fn get_env_or(var_name: &str, default: &str) -> String {
240 env::var(var_name).unwrap_or_else(|_| default.to_string())
241}
242
243pub fn run_with_diagnostics<F>(f: F)
248where
249 F: FnOnce() -> anyhow::Result<()>,
250{
251 if let Err(report) = f() {
252 eprintln!("\n{}", "=".repeat(80).red());
253 eprintln!("{}", "Application Error".red().bold());
254 eprintln!("{}", "=".repeat(80).red());
255 eprintln!();
256
257 if let Some(shell_err) = report.downcast_ref::<ShellExecError>() {
259 eprintln!("{}", shell_err.format_detailed());
260 } else {
261 eprintln!("{:?}", report);
263 }
264
265 eprintln!();
266 eprintln!("{}", "Package Information:".cyan().bold());
267 eprintln!(" Name: {}", env!("CARGO_PKG_NAME"));
268 eprintln!(" Version: {}", env!("CARGO_PKG_VERSION"));
269 eprintln!(" Authors: {}", env!("CARGO_PKG_AUTHORS"));
270 eprintln!(" Description: {}", env!("CARGO_PKG_DESCRIPTION"));
271 eprintln!(" Homepage: {}", env!("CARGO_PKG_HOMEPAGE"));
272 eprintln!(" Repository: {}", env!("CARGO_PKG_REPOSITORY"));
273 eprintln!();
274 std::process::exit(1);
275 }
276}
277
278#[macro_export]
288macro_rules! trap_panics_and_errors {
289 ($error_id:expr, $main:expr) => {{
290 use colored::Colorize;
291 use std::panic;
292
293 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
294 $crate::run_with_diagnostics(|| {
295 $main().map_err(|e: Box<dyn std::error::Error>| {
296 anyhow::anyhow!("[{}] {}", $error_id, e)
297 })
298 });
299 }));
300
301 if let Err(panic_info) = result {
302 eprintln!("\n{}", "=".repeat(80).red().bold());
303 eprintln!("{}", "PANIC OCCURRED".red().bold());
304 eprintln!("{}", "=".repeat(80).red().bold());
305 eprintln!("Error ID: {}", $error_id.to_string().green());
306 eprintln!("Panic Info: {:?}", panic_info);
307 eprintln!();
308 eprintln!("{}", "Package Information:".cyan().bold());
309 eprintln!(" Name: {}", env!("CARGO_PKG_NAME"));
310 eprintln!(" Version: {}", env!("CARGO_PKG_VERSION"));
311 eprintln!();
312 std::process::exit(101);
313 }
314 }};
315}
316
317#[macro_export]
324macro_rules! exec {
325 ($error_id:expr, $verbose:expr, $($cmd:tt)*) => {{
326 use colored::Colorize;
327 let formatted_str = format!($($cmd)*);
328 if $verbose {
329 eprintln!(
330 "{}",
331 format!("[{}] {}", $error_id, formatted_str).magenta()
332 );
333 }
334 $crate::execute_command(&formatted_str, $error_id)
335 }};
336}
337
338#[macro_export]
345macro_rules! s {
346 ($error_id:expr, $($cmd:tt)*) => {{
347 use colored::Colorize;
348 use log::{debug, info, error};
349 let formatted_str = format!($($cmd)*);
350 info!("{}", format!("[{}] Executing: {}", $error_id, formatted_str).magenta());
351
352 let result = $crate::execute_command(&formatted_str, $error_id);
353
354 match &result {
355 Ok(output) => debug!("Output: {}", output),
356 Err(e) => {
357 if let Some(shell_err) = e.downcast_ref::<$crate::ShellExecError>() {
358 error!("{}", shell_err.format_detailed());
359 } else {
360 error!("Error: {:?}", e);
361 }
362 }
363 }
364
365 result
366 }};
367}
368
369#[macro_export]
376macro_rules! e {
377 ($($cmd:tt)*) => {{
378 let formatted_str = format!($($cmd)*);
379 $crate::execute_command(&formatted_str, "no-error-id")
380 .expect(&format!("Command failed: {}", formatted_str))
381 }};
382}
383
384#[macro_export]
392macro_rules! a {
393 ($error_id:expr, $duration:expr, $($cmd:tt)*) => {{
394 use std::{thread, time};
395 use colored::Colorize;
396 use log::{debug, info, error};
397
398 let formatted_str = format!($($cmd)*);
399 info!("{}", format!("[{}] Executing with timeout: {}", $error_id, formatted_str).magenta());
400
401 let error_id_clone = $error_id.to_string();
402 let cmd_clone = formatted_str.clone();
403
404 let handle = thread::spawn(move || {
405 $crate::execute_command(&cmd_clone, &error_id_clone)
406 });
407
408 let check_interval = time::Duration::from_millis(10);
409 let start = time::Instant::now();
410
411 let result = loop {
412 if handle.is_finished() {
413 break match handle.join() {
414 Ok(result) => {
415 match &result {
416 Ok(output) => debug!("Output: {}", output),
417 Err(e) => {
418 if let Some(shell_err) = e.downcast_ref::<$crate::ShellExecError>() {
419 error!("{}", shell_err.format_detailed());
420 } else {
421 error!("Error: {:?}", e);
422 }
423 }
424 }
425 result
426 }
427 Err(_) => {
428 Err($crate::ShellExecError::JoinFailed {
429 command: formatted_str.clone(),
430 error_id: $error_id.to_string(),
431 }.into())
432 }
433 };
434 }
435
436 thread::sleep(check_interval);
437
438 if start.elapsed() >= $duration {
439 let duration_ms = $duration.as_millis() as u64;
441 break Err($crate::ShellExecError::Timeout {
442 command: formatted_str.clone(),
443 duration_ms,
444 error_id: $error_id.to_string(),
445 }.into());
446 }
447 };
448
449 result
450 }};
451}
452
453pub fn read_prompt(prompt: &str) -> String {
461 use std::io::{self, Write};
462
463 print!("{}", prompt);
464 io::stdout().flush().expect("Failed to flush stdout");
465
466 let mut buffer = String::new();
467 io::stdin()
468 .read_line(&mut buffer)
469 .expect("Failed to read from stdin");
470
471 buffer.trim().to_string()
472}
473
474pub fn read_prompt_result(prompt: &str) -> anyhow::Result<String> {
483 use std::io::{self, Write};
484
485 print!("{}", prompt);
486 io::stdout()
487 .flush()
488 .map_err(|e| anyhow::anyhow!("Failed to flush stdout: {}", e))?;
489
490 let mut buffer = String::new();
491 io::stdin()
492 .read_line(&mut buffer)
493 .map_err(|e| anyhow::anyhow!("Failed to read from stdin: {}", e))?;
494
495 Ok(buffer.trim().to_string())
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_successful_command() {
504 let output = execute_command("echo Hello World", "test-001").unwrap();
505 assert_eq!(output.trim(), "Hello World");
506 }
507
508 #[test]
509 fn test_successful_command_raw() {
510 let output = execute_command_raw("echo Hello World", "test-002").unwrap();
511 assert_eq!(output.stdout.trim(), "Hello World");
512 assert!(output.success());
513 assert!(output.stdout().is_some());
514 }
515
516 #[test]
517 fn test_exec_macro() {
518 let output = exec!("test-003", false, "echo {}", "Hello World").unwrap();
519 assert_eq!(output.trim(), "Hello World");
520 }
521
522 #[test]
523 fn test_e_macro() {
524 let output = e!("echo test");
525 assert_eq!(output.trim(), "test");
526 }
527
528 #[test]
529 fn test_failing_command() {
530 let result = execute_command("nonexistent_command_xyz", "test-004");
531 assert!(result.is_err());
532
533 if let Err(e) = result {
534 let error_string = format!("{:?}", e);
535 assert!(error_string.contains("nonexistent_command_xyz"));
536 }
537 }
538
539 #[test]
540 fn test_command_output_options() {
541 let output = execute_command_raw("echo test", "test-005").unwrap();
542 assert!(output.stdout().is_some());
543 assert_eq!(output.stdout().unwrap().trim(), "test");
544
545 assert!(output.stderr().is_none() || output.stderr().unwrap().is_empty());
547 }
548
549 #[test]
550 fn test_get_env_or() {
551 let value = get_env_or("NONEXISTENT_VAR_XYZ", "default_value");
552 assert_eq!(value, "default_value");
553
554 unsafe { std::env::set_var("TEST_VAR_XYZ", "test_value") };
556 let value = get_env_or("TEST_VAR_XYZ", "default");
557 assert_eq!(value, "test_value");
558 }
559
560 #[test]
561 fn test_timeout_macro() {
562 use std::time::Duration;
563
564 let result = a!("test-006", Duration::from_secs(5), "echo fast");
566 assert!(result.is_ok());
567 }
568
569 #[test]
570 fn test_formatted_error() {
571 let result = execute_command("nonexistent_xyz_123", "format-test");
573 assert!(result.is_err());
574
575 if let Err(e) = result {
576 if let Some(shell_err) = e.downcast_ref::<ShellExecError>() {
577 let formatted = shell_err.format_detailed();
578 assert!(formatted.contains("Command Execution Failed"));
579 assert!(formatted.contains("nonexistent_xyz_123"));
580 assert!(formatted.contains("format-test"));
581 }
582 }
583 }
584}