Skip to main content

double_o/
exec.rs

1use std::ffi::OsString;
2use std::process::Command;
3
4use crate::error::Error;
5
6/// Output from executing a shell command.
7///
8/// Captures the standard output, standard error, and exit status of a command
9/// executed via `run()`.
10pub struct CommandOutput {
11    /// Standard output as raw bytes.
12    pub stdout: Vec<u8>,
13
14    /// Standard error as raw bytes.
15    pub stderr: Vec<u8>,
16
17    /// Process exit code (0 indicates success).
18    pub exit_code: i32,
19}
20
21impl CommandOutput {
22    /// Merged output: stdout followed by stderr.
23    pub fn merged(&self) -> Vec<u8> {
24        let mut out = self.stdout.clone();
25        out.extend_from_slice(&self.stderr);
26        out
27    }
28
29    /// Merged output as a lossy UTF-8 string.
30    pub fn merged_lossy(&self) -> String {
31        String::from_utf8_lossy(&self.merged()).into_owned()
32    }
33}
34
35/// Execute a shell command and capture its output.
36///
37/// Runs the first argument as the program name with remaining arguments as parameters.
38/// Captures stdout, stderr, and exit status.
39///
40/// # Arguments
41///
42/// * `args` - Command arguments where the first element is the program name
43///
44/// # Returns
45///
46/// A `CommandOutput` containing stdout, stderr, and exit code
47///
48/// # Errors
49///
50/// Returns an error if the command cannot be spawned or if there's an I/O error during execution.
51///
52/// # Examples
53///
54/// ```
55/// use double_o::exec::run;
56///
57/// let output = run(&["echo".into(), "hello".into()]).unwrap();
58/// assert_eq!(output.exit_code, 0);
59/// assert!(String::from_utf8_lossy(&output.stdout).contains("hello"));
60/// ```
61pub fn run(args: &[OsString]) -> Result<CommandOutput, Error> {
62    let output = Command::new(&args[0]).args(&args[1..]).output()?;
63
64    let exit_code = output.status.code().unwrap_or(128);
65
66    Ok(CommandOutput {
67        stdout: output.stdout,
68        stderr: output.stderr,
69        exit_code,
70    })
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_successful_command() {
79        let result = run(&["echo".into(), "hello".into()]).unwrap();
80        assert_eq!(result.exit_code, 0);
81        assert_eq!(String::from_utf8_lossy(&result.stdout), "hello\n");
82    }
83
84    #[test]
85    fn test_failing_command() {
86        let result = run(&["false".into()]).unwrap();
87        assert_ne!(result.exit_code, 0);
88    }
89
90    #[test]
91    fn test_stderr_captured() {
92        let result = run(&["sh".into(), "-c".into(), "echo err >&2".into()]).unwrap();
93        assert_eq!(result.exit_code, 0);
94        assert_eq!(String::from_utf8_lossy(&result.stderr), "err\n");
95    }
96
97    #[test]
98    fn test_exit_code_preserved() {
99        let result = run(&["sh".into(), "-c".into(), "exit 42".into()]).unwrap();
100        assert_eq!(result.exit_code, 42);
101    }
102
103    #[test]
104    fn test_merged_output() {
105        let result = run(&["sh".into(), "-c".into(), "echo out; echo err >&2".into()]).unwrap();
106        let merged = result.merged_lossy();
107        assert!(merged.contains("out"));
108        assert!(merged.contains("err"));
109    }
110}