Skip to main content

command_stream/execa/
result.rs

1use std::fmt;
2use std::time::Duration;
3
4/// Execa-like result metadata, with byte-preserving output.
5#[derive(Debug, Clone, Default)]
6pub struct ExecaResult {
7    pub stdout: Vec<u8>,
8    pub stderr: Vec<u8>,
9    pub all: Option<Vec<u8>>,
10    pub exit_code: Option<i32>,
11    pub signal: Option<String>,
12    pub failed: bool,
13    pub killed: bool,
14    pub timed_out: bool,
15    pub is_canceled: bool,
16    pub is_max_buffer: bool,
17    pub command: String,
18    pub escaped_command: String,
19    pub duration: Duration,
20    pub cause: Option<String>,
21}
22
23impl ExecaResult {
24    /// Lossy UTF-8 view of stdout; use `stdout` directly for exact bytes.
25    pub fn text(&self) -> String {
26        String::from_utf8_lossy(&self.stdout).into_owned()
27    }
28
29    pub fn stderr_text(&self) -> String {
30        String::from_utf8_lossy(&self.stderr).into_owned()
31    }
32
33    pub fn stdout_lines(&self) -> Vec<String> {
34        lines(&self.stdout)
35    }
36
37    pub fn stderr_lines(&self) -> Vec<String> {
38        lines(&self.stderr)
39    }
40}
41
42fn lines(bytes: &[u8]) -> Vec<String> {
43    if bytes.is_empty() {
44        return Vec::new();
45    }
46    String::from_utf8_lossy(bytes)
47        .split('\n')
48        .map(|line| line.strip_suffix('\r').unwrap_or(line).to_string())
49        .collect()
50}
51
52pub(crate) fn strip_final_newline(bytes: &mut Vec<u8>) {
53    if bytes.last() == Some(&b'\n') {
54        bytes.pop();
55        if bytes.last() == Some(&b'\r') {
56            bytes.pop();
57        }
58    } else if bytes.last() == Some(&b'\r') {
59        bytes.pop();
60    }
61}
62
63/// A failed command retains the same metadata as a successful result.
64#[derive(Debug, Clone)]
65pub struct ExecaError {
66    pub result: Box<ExecaResult>,
67}
68
69impl fmt::Display for ExecaError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        let result = &self.result;
72        let reason = if result.timed_out {
73            "timed out".to_string()
74        } else if result.is_canceled {
75            "canceled".to_string()
76        } else if result.is_max_buffer {
77            "exceeded max_buffer".to_string()
78        } else if let Some(cause) = &result.cause {
79            cause.clone()
80        } else if let Some(signal) = &result.signal {
81            format!("killed with {signal}")
82        } else {
83            format!("failed with exit code {}", result.exit_code.unwrap_or(-1))
84        };
85        write!(f, "Command {reason}: {}", result.command)
86    }
87}
88
89impl std::error::Error for ExecaError {}
90
91pub type ExecaOutcome = Result<ExecaResult, ExecaError>;