use alloc::borrow::Cow;
use std::{
ffi::OsStr,
io::{self, Write},
path::PathBuf,
process::{Child, Command, Stdio},
};
use getset::{Getters, Setters, WithSetters};
use tap::Pipe;
use crate::{
bool_ext::BoolExt,
os_cmd::{DecodedText, MiniStr, Runner},
};
pub type CowOsStrVec<'a, const N: usize> = tinyvec::TinyVec<[Cow<'a, OsStr>; N]>;
fn err_invalid_input(msg: &str) -> io::Error {
let kind = io::ErrorKind::InvalidInput;
io::Error::new(kind, msg)
}
fn err_empty_command() -> io::Error {
err_invalid_input("empty command argv")
}
pub(crate) fn err_failed_to_run(program: Option<&OsStr>) -> io::Error {
format!("Failed to run command: {program:?}") .pipe(io::Error::other)
}
pub fn run_os_cmd<I>(into_iter: I) -> io::Result<()>
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
let mut iter = into_iter.into_iter();
let program = iter
.next()
.ok_or_else(err_empty_command)?
.as_ref()
.to_os_string();
let failed_to_run = || err_failed_to_run(Some(&program));
Command::new(&program) .args(iter) .status()? .success() .then_ok_or_else(failed_to_run) }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StdioMode {
#[default]
Inherit,
Piped,
Null,
}
impl From<StdioMode> for Stdio {
fn from(val: StdioMode) -> Self {
use StdioMode::*;
match val {
Inherit => Stdio::inherit(),
Piped => Stdio::piped(),
Null => Stdio::null(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, WithSetters, Setters, Getters)]
#[getset(set = "pub", set_with = "pub", get = "pub with_prefix")]
pub struct CommandSpawner<'a> {
stdin: StdioMode,
stdout: StdioMode,
stderr: StdioMode,
argv: CowOsStrVec<'a, 9>,
stdin_data: Option<&'a [u8]>,
envs: Option<Box<[(MiniStr, MiniStr)]>>,
working_dir: Option<PathBuf>,
}
impl<'a> Default for CommandSpawner<'a> {
fn default() -> Self {
use StdioMode::*;
Self {
stdin: Inherit,
stdout: Inherit,
stderr: Inherit,
argv: Default::default(),
stdin_data: None,
envs: None,
working_dir: None,
}
}
}
impl<'a> CommandSpawner<'a> {
#[inline]
fn effective_stdin_mode(has_data: bool, stdin: StdioMode) -> StdioMode {
use StdioMode::*;
match (has_data, stdin) {
(true, _) => Piped,
_ => stdin,
}
}
pub fn spawn(self) -> io::Result<Child> {
let Self {
argv: command,
stdin_data,
stdin,
stdout: stdout_mode,
stderr: stderr_mode,
envs: environment_vars,
working_dir,
..
} = self;
let stdin_mode = Self::effective_stdin_mode(stdin_data.is_some(), stdin);
command
.into_iter()
.pipe(|mut iter| {
iter
.next()
.ok_or_else(err_empty_command)
.map(|prog| (prog, iter))
})?
.pipe(|(prog, iter)| {
prog
.pipe(Command::new)
.args(iter)
.stdin(stdin_mode)
.stdout(stdout_mode)
.stderr(stderr_mode)
.pipe(|x| match environment_vars {
Some(map) => x.envs(map),
_ => x,
})
.pipe(|x| match working_dir {
Some(p) => x.current_dir(p),
_ => x,
})
.spawn()
})?
.pipe(|child| Self::write_child_stdin(child, stdin_data))
}
pub fn write_child_stdin(
mut child: Child,
stdin_data: Option<&[u8]>,
) -> io::Result<Child> {
if let Some(data) = stdin_data {
child
.stdin
.as_mut()
.ok_or_else(|| err_invalid_input("Failed to access child's stdin."))?
.write_all(data)?
}
Ok(child)
}
#[inline]
pub fn capture_raw_output(
self,
cap_out: bool,
cap_err: bool,
) -> io::Result<std::process::Output> {
match (cap_out, cap_err) {
(true, true) => self
.with_stdout(StdioMode::Piped)
.with_stderr(StdioMode::Piped),
(true, false) => self.with_stdout(StdioMode::Piped),
(false, true) => self.with_stderr(StdioMode::Piped),
_ => self,
}
.spawn()?
.wait_with_output()
}
pub fn capture_stdout(self) -> io::Result<DecodedText> {
self
.capture_raw_output(true, false)?
.stdout
.pipe(DecodedText::from_vec)
.pipe(Ok)
}
pub fn capture_stderr(self) -> io::Result<DecodedText> {
self
.capture_raw_output(false, true)?
.stderr
.pipe(DecodedText::from_vec)
.pipe(Ok)
}
pub fn capture_stdout_and_stderr(self) -> io::Result<[DecodedText; 2]> {
self
.capture_raw_output(true, true)?
.pipe(|o| [o.stdout, o.stderr])
.map(DecodedText::from_vec)
.pipe(Ok)
}
}
impl<'a, T> From<T> for CommandSpawner<'a>
where
T: Into<Runner<'a>>,
{
fn from(value: T) -> Self {
let Runner {
command,
remove_comments,
stdin_data,
..
} = value.into();
command
.into_tinyvec(remove_comments)
.into_iter()
.map(super::cow_str_into_cow_osstr)
.collect::<CowOsStrVec<_>>()
.pipe(|x| CommandSpawner::default().with_argv(x))
.with_stdin_data(stdin_data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn capture_stdout_hello() -> io::Result<()> {
let v = ["printf", "%s", "hello"]
.pipe(CommandSpawner::from)
.capture_stdout()?;
assert_eq!(v.data(), "hello");
Ok(())
}
#[test]
#[cfg(target_os = "linux")]
fn pass_stdin_data() -> io::Result<()> {
let [stdout, stderr] = "wc -m"
.pipe(CommandSpawner::from)
.with_stdin_data(Some("world".as_bytes()))
.capture_stdout_and_stderr()?;
assert_eq!(&*stdout, "5\n");
assert_eq!(&*stderr, "");
Ok(())
}
}