Skip to main content

command_stream/zx/
output.rs

1//! [`ProcessOutput`]: the settled result of a zx command.
2
3use std::fmt;
4use std::path::Path;
5use std::time::Duration;
6
7use regex::Regex;
8use serde::de::DeserializeOwned;
9
10use super::error::{exit_code_info, ERROR_DETAILS_LIMIT};
11use super::error::{format_error_details, format_error_message, format_exit_message};
12
13/// Why a command could not be run at all (as opposed to exiting non-zero).
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct ErrorInfo {
16    /// Human readable description.
17    pub message: String,
18    /// Negative errno value (libuv style), when known.
19    pub errno: Option<i64>,
20    /// Symbolic error code such as `ENOENT`, when known.
21    pub code: Option<String>,
22}
23
24impl ErrorInfo {
25    /// An error described only by its message.
26    pub fn new(message: impl Into<String>) -> Self {
27        Self {
28            message: message.into(),
29            errno: None,
30            code: None,
31        }
32    }
33
34    /// Convert an I/O error, keeping its errno and symbolic code.
35    pub fn from_io(err: &std::io::Error) -> Self {
36        let raw = err.raw_os_error();
37        let code = match err.kind() {
38            std::io::ErrorKind::NotFound => Some("ENOENT"),
39            std::io::ErrorKind::PermissionDenied => Some("EACCES"),
40            std::io::ErrorKind::AlreadyExists => Some("EEXIST"),
41            std::io::ErrorKind::BrokenPipe => Some("EPIPE"),
42            std::io::ErrorKind::InvalidInput => Some("EINVAL"),
43            _ => None,
44        };
45        Self {
46            message: err.to_string(),
47            errno: raw.map(|e| -i64::from(e)),
48            code: code.map(str::to_string),
49        }
50    }
51}
52
53/// The result of running a command: captured streams, exit status and timing.
54///
55/// `stdall` holds stdout and stderr interleaved in arrival order. Following
56/// zx, the textual accessors ([`text`](Self::text), [`lines`](Self::lines),
57/// [`json`](Self::json), [`buffer`](Self::buffer) and `Display`) read
58/// `stdall`, which equals `stdout` whenever the command wrote no stderr.
59#[derive(Debug, Clone, Default, PartialEq)]
60pub struct ProcessOutput {
61    /// Captured standard output.
62    pub stdout: String,
63    /// Captured standard error.
64    pub stderr: String,
65    /// Standard output and error interleaved in arrival order.
66    pub stdall: String,
67    /// Exit code; `None` when the process was killed by a signal or never ran.
68    pub exit_code: Option<i32>,
69    /// Name of the terminating signal (for example `SIGTERM`).
70    pub signal: Option<String>,
71    /// Wall-clock run time.
72    pub duration: Duration,
73    /// Set when the command could not be started.
74    pub error: Option<ErrorInfo>,
75    /// Where the command came from (the command text by default).
76    pub from: String,
77}
78
79impl ProcessOutput {
80    /// Build an output from its main parts.
81    pub fn new(
82        exit_code: Option<i32>,
83        signal: Option<&str>,
84        stdout: impl Into<String>,
85        stderr: impl Into<String>,
86        stdall: impl Into<String>,
87    ) -> Self {
88        Self {
89            stdout: stdout.into(),
90            stderr: stderr.into(),
91            stdall: stdall.into(),
92            exit_code,
93            signal: signal.map(str::to_string),
94            ..Default::default()
95        }
96    }
97
98    /// An output describing a command that could not be run.
99    pub fn from_error(error: ErrorInfo, from: impl Into<String>) -> Self {
100        Self {
101            error: Some(error),
102            from: from.into(),
103            ..Default::default()
104        }
105    }
106
107    /// Set the `from` location used in messages.
108    pub fn with_from(mut self, from: impl Into<String>) -> Self {
109        self.from = from.into();
110        self
111    }
112
113    /// Set the duration.
114    pub fn with_duration(mut self, duration: Duration) -> Self {
115        self.duration = duration;
116        self
117    }
118
119    /// `true` when the command ran and exited with code 0.
120    pub fn ok(&self) -> bool {
121        self.error.is_none() && self.exit_code == Some(0)
122    }
123
124    /// The combined output as text.
125    pub fn text(&self) -> String {
126        self.stdall.clone()
127    }
128
129    /// The combined output hex-encoded (zx `text('hex')`).
130    pub fn text_hex(&self) -> String {
131        self.stdall.bytes().map(|b| format!("{b:02x}")).collect()
132    }
133
134    /// The combined output, trimmed (zx `valueOf()`).
135    pub fn value_of(&self) -> &str {
136        self.stdall.trim()
137    }
138
139    /// The combined output as bytes.
140    pub fn buffer(&self) -> Vec<u8> {
141        self.stdall.as_bytes().to_vec()
142    }
143
144    /// Parse the combined output as JSON.
145    pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
146        serde_json::from_str(&self.stdall)
147    }
148
149    /// Split the combined output on `\r?\n`, dropping a trailing empty piece.
150    pub fn lines(&self) -> Vec<String> {
151        static NEWLINE: once_cell::sync::Lazy<Regex> =
152            once_cell::sync::Lazy::new(|| Regex::new(r"\r?\n").expect("valid regex"));
153        finish_lines(NEWLINE.split(&self.stdall).map(str::to_string).collect())
154    }
155
156    /// Split the combined output on a custom delimiter.
157    pub fn lines_with(&self, delimiter: &str) -> Vec<String> {
158        finish_lines(self.stdall.split(delimiter).map(str::to_string).collect())
159    }
160
161    /// Description of the exit code (for example `Command not found`).
162    pub fn exit_code_info(&self) -> Option<&'static str> {
163        self.exit_code.and_then(exit_code_info)
164    }
165
166    /// The zx error message for this output.
167    pub fn message(&self) -> String {
168        if let Some(err) = &self.error {
169            return format_error_message(&err.message, err.errno, err.code.as_deref(), &self.from);
170        }
171        let details = if self.stderr.trim().is_empty() {
172            format_error_details(&self.lines(), ERROR_DETAILS_LIMIT)
173        } else {
174            String::new()
175        };
176        format_exit_message(
177            self.exit_code,
178            self.signal.as_deref(),
179            &self.stderr,
180            &self.from,
181            &details,
182        )
183    }
184}
185
186fn finish_lines(mut pieces: Vec<String>) -> Vec<String> {
187    if pieces.last().is_some_and(|l| l.is_empty()) {
188        pieces.pop();
189    }
190    pieces
191}
192
193impl fmt::Display for ProcessOutput {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        f.write_str(&self.stdall)
196    }
197}
198
199impl std::error::Error for ProcessOutput {}
200
201impl AsRef<Path> for ProcessOutput {
202    /// The trimmed combined output as a path (handy for `cd(&output)`).
203    fn as_ref(&self) -> &Path {
204        Path::new(self.stdall.trim())
205    }
206}
207
208impl IntoIterator for &ProcessOutput {
209    type Item = String;
210    type IntoIter = std::vec::IntoIter<String>;
211
212    fn into_iter(self) -> Self::IntoIter {
213        self.lines().into_iter()
214    }
215}