use crate::{CmdError, CommandWithName, NamedOutput, OutputWithName};
use regex::Regex;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::process::{Command, Output};
use std::sync::LazyLock;
use std::{io, process, thread};
use std::{mem, panic};
static QUOTE_ARG_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([^A-Za-z0-9_\-.,:/@\n])").expect("clippy checked"));
#[must_use]
pub fn display(command: &mut Command) -> String {
vec![command.get_program().to_string_lossy().to_string()]
.into_iter()
.chain(command.get_args().map(OsStr::to_string_lossy).map(|arg| {
if QUOTE_ARG_RE.is_match(&arg) {
format!("{arg:?}")
} else {
format!("{arg}")
}
}))
.collect::<Vec<String>>()
.join(" ")
}
#[cfg(command_resolved_envs)]
#[must_use]
pub fn display_env_vars<T, K>(cmd: &mut Command, keys: T) -> String
where
T: IntoIterator<Item = K>,
K: Into<OsString>,
{
let env: HashMap<OsString, OsString> = cmd.get_resolved_envs().collect();
display_with_env_keys(cmd, env, keys)
}
#[must_use]
pub fn display_with_env_keys<E, K, V, I, O>(cmd: &mut Command, env: E, keys: I) -> String
where
E: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
I: IntoIterator<Item = O>,
O: Into<OsString>,
{
display_name_with_env_keys(cmd.name(), env, keys)
}
pub(crate) fn display_name_with_env_keys<E, K, V, I, O>(name: String, env: E, keys: I) -> String
where
E: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
I: IntoIterator<Item = O>,
O: Into<OsString>,
{
let env = env
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect::<HashMap<OsString, OsString>>();
keys.into_iter()
.map(|key| {
let key = key.into();
format!(
"{}={:?}",
key.to_string_lossy(),
env.get(&key).cloned().unwrap_or_else(|| OsString::from(""))
)
})
.chain([name])
.collect::<Vec<String>>()
.join(" ")
}
#[cfg(not(any(unix, windows)))]
compile_error!(
"fun_run constructs `ExitStatus` values and only supports `unix` and `windows` targets"
);
#[must_use]
pub fn on_system_error(name: String, error: std::io::Error) -> CmdError {
CmdError::SystemError(name, error)
}
pub fn nonzero_streamed(name: String, output: impl Into<Output>) -> Result<NamedOutput, CmdError> {
let output = output.into();
if output.status.success() {
Ok(output.named(name))
} else {
Err(CmdError::NonZeroExitAlreadyStreamed(output.named(name)))
}
}
pub fn nonzero_captured(name: String, output: impl Into<Output>) -> Result<NamedOutput, CmdError> {
let output = output.into();
if output.status.success() {
Ok(output.named(name))
} else {
Err(CmdError::NonZeroExitNotStreamed(output.named(name)))
}
}
pub(crate) fn output_and_write_streams<OW: Write + Send, EW: Write + Send>(
command: &mut Command,
stdout_write: OW,
stderr_write: EW,
) -> io::Result<process::Output> {
let mut stdout_buffer = Vec::new();
let mut stderr_buffer = Vec::new();
let mut stdout = tee(&mut stdout_buffer, stdout_write);
let mut stderr = tee(&mut stderr_buffer, stderr_write);
let mut child = command
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.spawn()?;
thread::scope(|scope| {
let stdout_thread = mem::take(&mut child.stdout).map(|mut child_stdout| {
scope.spawn(move || std::io::copy(&mut child_stdout, &mut stdout))
});
let stderr_thread = mem::take(&mut child.stderr).map(|mut child_stderr| {
scope.spawn(move || std::io::copy(&mut child_stderr, &mut stderr))
});
stdout_thread
.map_or_else(
|| Ok(0),
|handle| match handle.join() {
Ok(value) => value,
Err(err) => panic::resume_unwind(err),
},
)
.and({
stderr_thread.map_or_else(
|| Ok(0),
|handle| match handle.join() {
Ok(value) => value,
Err(err) => panic::resume_unwind(err),
},
)
})
.and_then(|_| child.wait())
})
.map(|status| process::Output {
status,
stdout: stdout_buffer,
stderr: stderr_buffer,
})
}
pub(crate) fn tee<A: io::Write, B: io::Write>(a: A, b: B) -> TeeWrite<A, B> {
TeeWrite {
inner_a: a,
inner_b: b,
}
}
#[derive(Debug, Clone)]
pub(crate) struct TeeWrite<A: io::Write, B: io::Write> {
inner_a: A,
inner_b: B,
}
impl<A: io::Write, B: io::Write> io::Write for TeeWrite<A, B> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner_a.write_all(buf)?;
self.inner_b.write_all(buf)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.inner_a.flush()?;
self.inner_b.flush()
}
}
#[cfg(all(test, unix))]
mod test {
use super::*;
use pretty_assertions::assert_str_eq;
use std::process::Command;
#[test]
fn test_output_and_write_streams_stdout() {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
let mut cmd = Command::new("echo");
cmd.args(["-n", "Hello World!"]);
let output = output_and_write_streams(&mut cmd, &mut stdout_buf, &mut stderr_buf).unwrap();
assert_eq!(stdout_buf, "Hello World!".as_bytes());
assert_eq!(stderr_buf, Vec::<u8>::new());
assert_eq!(output.status.code(), Some(0));
assert_eq!(output.stdout, "Hello World!".as_bytes());
assert_eq!(output.stderr, Vec::<u8>::new());
}
#[test]
fn test_output_and_write_streams_stderr() {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
let mut cmd = Command::new("bash");
cmd.args(["-c", "echo -n Hello World! >&2"]);
let _ = output_and_write_streams(&mut cmd, &mut stdout_buf, &mut stderr_buf).unwrap();
assert_str_eq!(&String::from_utf8_lossy(&stderr_buf), "Hello World!");
}
}