use std::{
ffi::OsStr,
fmt::{self, Write},
process::{self, Child, Command, Output},
};
use crate::{utf8_to_string_lossy, CommandFailed, InstallError};
pub trait CommandExt {
fn formattable(&self) -> CommandFormattable<'_>;
fn output_checked_status(&mut self) -> Result<Output, InstallError>;
fn spawn_with_cmd(self) -> Result<ChildWithCommand, InstallError>;
}
impl CommandExt for Command {
fn formattable(&self) -> CommandFormattable<'_> {
CommandFormattable(self)
}
fn output_checked_status(&mut self) -> Result<Output, InstallError> {
self.output()
.map_err(InstallError::from)
.and_then(|output| check_status(self, output))
}
fn spawn_with_cmd(mut self) -> Result<ChildWithCommand, InstallError> {
self.spawn()
.map_err(InstallError::from)
.map(move |child| ChildWithCommand { child, cmd: self })
}
}
pub struct CommandFormattable<'a>(&'a Command);
fn needs_escape(s: &str) -> bool {
s.contains(|ch| !matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+'))
}
fn write_shell_arg_escaped(f: &mut fmt::Formatter<'_>, os_str: &OsStr) -> fmt::Result {
let s = os_str.to_string_lossy();
if needs_escape(&s) {
f.write_str("\"")?;
for ch in s.chars() {
if ch == '"' {
f.write_char('\\')?;
}
f.write_char(ch)?;
}
f.write_str("\"")
} else {
f.write_str(&s)
}
}
impl fmt::Display for CommandFormattable<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let cmd = self.0;
write_shell_arg_escaped(f, cmd.get_program())?;
for arg in cmd.get_args() {
f.write_str(" ")?;
write_shell_arg_escaped(f, arg)?;
}
Ok(())
}
}
pub struct ChildWithCommand {
cmd: Command,
child: Child,
}
fn check_status(cmd: &Command, output: Output) -> Result<Output, InstallError> {
if output.status.success() {
Ok(output)
} else {
Err(CommandFailed {
command: cmd.formattable().to_string(),
stdout: utf8_to_string_lossy(output.stdout),
stderr: utf8_to_string_lossy(output.stderr),
}
.into())
}
}
impl ChildWithCommand {
pub fn wait_with_output_checked_status(self) -> Result<Output, InstallError> {
let cmd = self.cmd;
self.child
.wait_with_output()
.map_err(InstallError::from)
.and_then(|output| check_status(&cmd, output))
}
pub fn stdin(&mut self) -> &mut Option<process::ChildStdin> {
&mut self.child.stdin
}
pub fn stdout(&mut self) -> &mut Option<process::ChildStdout> {
&mut self.child.stdout
}
pub fn stderr(&mut self) -> &mut Option<process::ChildStderr> {
&mut self.child.stderr
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cmd_format() {
assert_eq!(
Command::new("cargo")
.args(["binstall", "-V"])
.formattable()
.to_string(),
"cargo binstall -V"
);
}
}