sh_exec/
lib.rs

1use colored::*;
2use shells::sh;
3use std::env;
4use std::error::Error;
5use std::fmt;
6
7/// Custom error type for shell command execution
8#[derive(Debug, Clone)]
9pub struct ShellError<'a> {
10    command: String,
11    exit_code: i32,
12    stderr: String,
13    stdout: String,
14    error_id: &'a str,
15}
16
17impl ShellError<'_> {
18    /// Creates a new ShellError instance
19    pub fn new(
20        command: String,
21        exit_code: i32,
22        stderr: String,
23        stdout: String,
24        error_id: &'static str,
25    ) -> Self {
26        ShellError {
27            command,
28            exit_code,
29            stderr,
30            stdout,
31            error_id,
32        }
33    }
34}
35
36// Implement Display trait for ShellError
37impl fmt::Display for ShellError<'_> {
38    /// Formats the ShellError instance
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(
41            f,
42            "{}: '{}'\nsh_exec Exit code: {}\nsh_exec Error ID:  {}\n",
43            "Command failed".red(),
44            self.command,
45            self.exit_code,
46            self.error_id.green(),
47        )?;
48
49        if !self.stdout.is_empty() {
50            write!(f, "Standard output:\n{}\n", self.stdout.green())?;
51        }
52
53        if !self.stderr.is_empty() {
54            write!(f, "Standard error:\n{}\n", self.stderr.magenta())?;
55        }
56
57        Ok(())
58    }
59}
60
61impl Error for ShellError<'_> {}
62
63/// Executes a shell command and returns a Result containing the command's output
64pub fn execute_command(cmd: &str, error_id: &'static str) -> Result<String, ShellError<'static>> {
65    let command = cmd.to_string();
66    let (code, stdout, stderr) = sh!("{}", cmd);
67
68    // Check exit code
69    if code == 0 {
70        Ok(stdout)
71    } else {
72        let error = ShellError::new(command, code, stderr, stdout, error_id);
73        Err(error)
74    }
75}
76
77/// read the content of a given environment variable as a String
78/// if the environment variable does not exist, return a `ShellError`
79pub fn get_env(env: &str, error_id: &'static str) -> Result<String, ShellError<'static>> {
80    // Get VERSION from environment
81    match env::var(env) {
82        Err(e) => Err(ShellError {
83            command: format!("shell-exec: get_env({env})"),
84            exit_code: 0,
85            stderr: format!("Environment variable '{env}' is not defined: {e:#?}."),
86            stdout: "".to_string(),
87            error_id,
88        }),
89        Ok(value) => Ok(value),
90    }
91}
92
93// main_run is a simple wrapper that prints cargo-related information 
94// in case the function `run` returns an error
95// Recommendation: use macro `trap_panics_and_errors` instead
96pub fn main_run(run: fn() -> Result<(), Box<dyn Error>>) {
97    if let Err(e) = run() {
98        eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
99        eprintln!("Name: {}", env!("CARGO_PKG_NAME"));
100        eprintln!("Authors: {}", env!("CARGO_PKG_AUTHORS"));
101
102        // Optional fields
103        eprintln!("Description: {}", env!("CARGO_PKG_DESCRIPTION"));
104        eprintln!("Homepage: {}", env!("CARGO_PKG_HOMEPAGE"));
105        eprintln!("Repository: {}", env!("CARGO_PKG_REPOSITORY"));
106        eprintln!("{e}")
107    }
108}
109
110/// trap_panics_and_errors traps panics that might be issued when calling a given function
111/// It will print a nice error message in case a panic is trapped.
112/// This macro also traps errors, prints the error and exists the program with error code 1
113///
114/// NOTE
115///   the Err type returned by the given function must return an Err that implements the Display trait.
116#[macro_export]
117macro_rules! trap_panics_and_errors {
118    ($error_id:literal , $main:expr) => {
119        use std::process;
120        use std::error::Error;
121        use colored::*;
122        use log::*;
123        match std::panic::catch_unwind(|| {
124            match $main() {
125                Err(e) => {
126                    error!("{}: {}", "trap_panics_and_errors".red(), $error_id.green());
127                    error!("  Version: {}", env!("CARGO_PKG_VERSION"));
128                    error!("  Name: {}", env!("CARGO_PKG_NAME"));
129                    error!("  Authors: {}", env!("CARGO_PKG_AUTHORS"));
130
131                    // Optional fields
132                    error!("  Description: {}", env!("CARGO_PKG_DESCRIPTION"));
133                    error!("  Homepage: {}", env!("CARGO_PKG_HOMEPAGE"));
134                    error!("  Repository: {}", env!("CARGO_PKG_REPOSITORY"));
135                    error!("  Error: {e}");
136                    // Exit with error (non-zero)
137                    process::exit(1)
138                }
139                Ok(result) => result,
140            }
141        }) {
142            Ok(result) => result,
143            Err(e) => {
144                eprintln!(
145                    "Error id: {}, 31963-28837-7387. Error {}: {e:#?}!", $error_id,
146                    "Application panicked".red()
147                );
148                std::process::exit(101);
149            }
150        }
151    };
152}
153
154/// Execute a shell command and return the output
155/// The command is formatted using the given arguments
156/// The command is printed if the verbose flag is set to true (useful for debugging)
157#[macro_export]
158macro_rules! exec {
159    ($error_id:literal , $verbose:expr , $($cmd:tt )* ) => {{
160        use colored::Colorize;
161        let formatted_str = &format!($( $cmd )*);
162        if $verbose { eprintln!("{}", format!("exec!({},{})", $error_id, formatted_str ).magenta()) }
163        execute_command(formatted_str, $error_id)
164    }};
165}
166
167/// Execute a shell command and return the output
168/// The command is formatted using the given arguments
169/// The command is printed at the INFO level
170/// The output of the command is printed at the DEBUG level
171/// In case of an error, the error is printed at the ERROR level
172/// The error is returned
173#[macro_export]
174macro_rules! s {
175    ($error_id:literal , $($cmd:tt )* ) => {{
176        use colored::Colorize;
177        use log::{debug, info, error};
178        let formatted_str = &format!($( $cmd )*);
179        info!("{}", format!("s!({},{})", $error_id, formatted_str ).magenta());
180        let output = execute_command(formatted_str, $error_id);
181        // Log output
182        match output.clone() {
183            std::result::Result::Ok(output) => debug!("{}", output),
184            Err(e) => error!("{}", e),
185        }
186        output
187    }};
188}
189
190
191/// Execute a shell command and return the output
192/// Any error results in a panic!
193#[macro_export]
194macro_rules! e {
195    ($($cmd:tt )* ) => {{
196        let formatted_str = &format!($( $cmd )*);
197        let output = execute_command(formatted_str, "no-error-id-specified");
198        // Log output
199        match output.clone() {
200            std::result::Result::Ok(output) => output,
201            Err(e) => panic!("Error executing command {formatted_str}: {}", e),
202        }
203    }};
204}
205
206/// Execute a shell command and return the output
207/// The command is formatted using the given arguments
208/// The command is printed at the INFO level
209/// The output of the command is printed at the DEBUG level
210/// In case of an error, the error is printed at the ERROR level
211/// The error is returned
212#[macro_export]
213macro_rules! a {
214    ($error_id:literal , $duration:tt, $($cmd:tt )* ) => {{
215            use std::{thread, time};
216            use colored::Colorize;
217            use log::{debug, info, error};
218            let handle = thread::spawn( || {
219                let formatted_str = &format!($( $cmd )*);
220                info!("{}", format!("s!({},{})", $error_id, formatted_str ).magenta());
221                let output = execute_command(formatted_str, $error_id);
222                // Log output
223                match output.clone() {
224                    std::result::Result::Ok(output) => debug!("{}", output),
225                    std::result::Result::Err(e) => error!("{}", e),
226                }
227                output
228            });
229            let ten_millis = time::Duration::from_millis(10);
230            let now = time::Instant::now();
231            
232            loop {
233                if handle.is_finished() {
234                    break;
235                }
236                thread::sleep(ten_millis);
237                if now.elapsed() >= $duration {
238                    break;
239                }
240            }
241            if handle.is_finished() {
242                match handle.join() {
243                    std::result::Result::Ok(result) => {
244                        std::result::Result::Ok(result?)
245                    }
246                    std::result::Result::Err(e) => {
247                        let cmd = format!($( $cmd )*);
248                        let error = ShellError::new(cmd, -2, String::from("Join Failed"), String::from("Joining of command failed"), $error_id);
249                        std::result::Result::Err(error)    
250                    }
251                }
252            } else {    
253                let cmd = format!($( $cmd )*);
254                let error = ShellError::new(cmd, -1, String::from("Timeout"), String::from("Command timed out"), $error_id);
255                std::result::Result::Err(error)
256            }
257        
258    }};
259}
260
261use std::io;
262use std::io::Write;
263
264pub fn read_prompt(prompt: &str) -> String {
265    print!("{prompt}");
266    // flush stdout to ensure the prompt is displayed immediately
267    // before reading input
268    io::stdout().flush().unwrap();
269    // create a mutable String to store the input
270    // read a line from stdin and store it in the buffer
271    // expect is used to handle any potential errors
272    // while reading from stdin
273    let mut buffer = String::new();
274    io::stdin().read_line(&mut buffer).expect("Failed to read from stdin");
275    // remove the trailing newline character
276    // and return the trimmed string
277    buffer.trim().to_string()
278}
279
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_successful_command() {
287        let output = execute_command("echo Hello World", "8923-2323-2323").unwrap();
288        assert_eq!(output.trim(), "Hello World");
289    }
290
291    #[test]
292    fn test_successful_fmt() {
293        let output = exec!("8923-2323-2323", false, "echo Hello World").unwrap();
294        assert_eq!(output.trim(), "Hello World");
295    }
296
297    #[test]
298    fn test_successful_fmt2() {
299        let output = exec!("21236-28986-4446", true, "echo {}", "Hello World",).unwrap();
300        assert_eq!(output.trim(), "Hello World");
301    }
302
303    #[test]
304    fn test_failing_command() {
305        let result = execute_command("nonexistent_command", "8923-2323-3289");
306        assert!(result.is_err());
307        let error = result.unwrap_err();
308        assert_eq!(error.exit_code, 127);
309        assert!(!error.stderr.is_empty());
310    }
311}