use std::{
ffi::OsStr,
io::{self, Write},
process::{Command, Output, Stdio},
};
use anyhow::{Context, Result, bail};
use colored::Colorize;
pub trait ProcessExt {
fn exec(&mut self) -> Result<()>;
fn exec_capture(&mut self) -> Result<Output>;
}
impl ProcessExt for Command {
fn exec(&mut self) -> Result<()> {
print_command(self)?;
let status = self
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("failed to spawn process")?;
if status.success() {
Ok(())
} else {
bail!("command exited with status {status}");
}
}
fn exec_capture(&mut self) -> Result<Output> {
print_command(self)?;
let output = self.output().context("failed to spawn process")?;
io::stdout()
.write_all(&output.stdout)
.context("failed to forward stdout")?;
io::stderr()
.write_all(&output.stderr)
.context("failed to forward stderr")?;
if output.status.success() {
Ok(output)
} else {
bail!("command exited with status {}", output.status);
}
}
}
fn print_command(command: &Command) -> Result<()> {
let rendered = render_command(command);
let mut stderr = io::stderr().lock();
writeln!(stderr, "{}", rendered.purple()).context("failed to print command")?;
Ok(())
}
fn render_command(command: &Command) -> String {
let mut parts = Vec::new();
if let Some(dir) = command.get_current_dir() {
parts.push(format!("cd {} &&", shell_escape(dir.as_os_str())));
}
parts.push(shell_escape(command.get_program()));
parts.extend(command.get_args().map(shell_escape));
parts.join(" ")
}
fn shell_escape(value: &OsStr) -> String {
let value = value.to_string_lossy();
if !value.is_empty()
&& value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '_' | '-' | '.' | '=' | ':'))
{
value.into_owned()
} else {
format!("'{}'", value.replace('\'', "'\\''"))
}
}