use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::process::ExitStatus;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandOutput {
pub stdout: CommandOutputString,
pub stderr: CommandOutputString,
pub status: ExitStatus,
}
impl CommandOutput {
#[must_use]
pub fn normalize_backslash(self) -> Self {
Self {
stdout: self.stdout.normalize_backslash(),
stderr: self.stderr.normalize_backslash(),
status: self.status,
}
}
#[must_use]
pub fn normalize_stderr_exit_status(self) -> Self {
Self {
stdout: self.stdout,
stderr: self.stderr.normalize_exit_status(),
status: self.status,
}
}
#[must_use]
pub fn strip_stderr_last_line(self) -> Self {
Self {
stdout: self.stdout,
stderr: self.stderr.strip_last_line(),
status: self.status,
}
}
#[must_use]
pub fn take_stdout_n_lines(self, n: usize) -> Self {
Self {
stdout: self.stdout.take_n_lines(n),
stderr: self.stderr,
status: self.status,
}
}
#[must_use]
pub fn normalize_stdout_with(self, f: impl FnOnce(String) -> String) -> Self {
Self {
stdout: self.stdout.normalize_with(f),
stderr: self.stderr,
status: self.status,
}
}
#[must_use]
pub fn normalize_stderr_with(self, f: impl FnOnce(String) -> String) -> Self {
Self {
stdout: self.stdout,
stderr: self.stderr.normalize_with(f),
status: self.status,
}
}
#[track_caller]
pub fn success(self) -> Self {
assert!(self.status.success(), "{self}");
self
}
}
impl Display for CommandOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
stdout,
stderr,
status,
} = self;
write!(f, "{stdout}")?;
if !stderr.is_empty() {
writeln!(f, "------- stderr -------")?;
write!(f, "{stderr}")?;
}
if !status.success() {
if let Some(code) = status.code() {
writeln!(f, "[exit status: {code}]")?;
} else {
writeln!(f, "[{status}]")?;
}
}
Ok(())
}
}
#[derive(Clone)]
pub struct CommandOutputString {
pub(super) raw: String,
pub(super) normalized: String,
}
impl CommandOutputString {
#[must_use]
pub fn normalize_backslash(self) -> Self {
self.normalize_with(|s| s.replace('\\', "/"))
}
#[must_use]
pub fn normalize_exit_status(self) -> Self {
self.normalize_with(|s| s.replace("exit code:", "exit status:"))
}
#[must_use]
pub fn strip_last_line(self) -> Self {
self.normalize_with(|mut s| {
s.truncate(strip_last_line(&s).len());
s
})
}
#[must_use]
pub fn take_n_lines(self, n: usize) -> Self {
self.normalize_with(|s| s.split_inclusive('\n').take(n).collect())
}
#[must_use]
pub fn normalize_with(mut self, f: impl FnOnce(String) -> String) -> Self {
self.normalized = f(self.normalized);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.raw.is_empty()
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn normalized(&self) -> &str {
&self.normalized
}
#[must_use]
pub fn into_raw(self) -> String {
self.raw
}
}
impl Debug for CommandOutputString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.raw, f)
}
}
impl Display for CommandOutputString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return Ok(());
}
writeln!(f, "{}[EOF]", self.normalized)
}
}
impl Eq for CommandOutputString {}
impl PartialEq for CommandOutputString {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
}
pub fn strip_last_line(s: &str) -> &str {
s.trim_end_matches('\n')
.rsplit_once('\n')
.map_or(s, |(h, _)| &s[..h.len() + 1])
}