use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[derive(Clone, Eq, PartialEq)]
pub struct CommandRequest {
pub program: String,
pub arguments: Vec<String>,
pub current_dir: PathBuf,
pub environment: BTreeMap<String, String>,
}
impl std::fmt::Debug for CommandRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let redacted_environment: BTreeMap<_, _> = self
.environment
.keys()
.map(|key| (key, "[REDACTED]"))
.collect();
formatter
.debug_struct("CommandRequest")
.field("program", &self.program)
.field("arguments", &self.arguments)
.field("current_dir", &self.current_dir)
.field("environment", &redacted_environment)
.finish()
}
}
impl CommandRequest {
pub fn new(
program: impl Into<String>,
arguments: impl IntoIterator<Item = impl Into<String>>,
current_dir: impl Into<PathBuf>,
) -> Self {
Self {
program: program.into(),
arguments: arguments.into_iter().map(Into::into).collect(),
current_dir: current_dir.into(),
environment: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandResult {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
impl CommandResult {
pub fn redact(mut self, secrets: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
for secret in secrets {
let secret = secret.as_ref();
if secret.is_empty() {
continue;
}
self.stdout = self.stdout.replace(secret, "[REDACTED]");
self.stderr = self.stderr.replace(secret, "[REDACTED]");
}
self
}
pub fn require_success(self, description: &str) -> Result<Self> {
if self.status == 0 {
return Ok(self);
}
let detail = self.stderr.trim();
if detail.is_empty() {
bail!("{description} failed with exit status {}", self.status);
}
bail!(
"{description} failed with exit status {}: {detail}",
self.status
)
}
}
pub trait CommandRunner: Send + Sync {
fn execute(&self, request: &CommandRequest) -> Result<CommandResult>;
fn execute_inheriting_output(&self, request: &CommandRequest) -> Result<CommandResult> {
self.execute(request)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemCommandRunner;
impl CommandRunner for SystemCommandRunner {
fn execute(&self, request: &CommandRequest) -> Result<CommandResult> {
let output = command(request)
.output()
.with_context(|| format!("failed to execute `{}`", request.program))?;
Ok(CommandResult {
status: output.status.code().unwrap_or(1),
stdout: String::from_utf8(output.stdout)
.context("command stdout is not valid UTF-8")?,
stderr: String::from_utf8(output.stderr)
.context("command stderr is not valid UTF-8")?,
})
}
fn execute_inheriting_output(&self, request: &CommandRequest) -> Result<CommandResult> {
let status = command(request)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.with_context(|| format!("failed to execute `{}`", request.program))?;
Ok(CommandResult {
status: status.code().unwrap_or(1),
stdout: String::new(),
stderr: String::new(),
})
}
}
fn command(request: &CommandRequest) -> Command {
let mut command = Command::new(&request.program);
command
.args(&request.arguments)
.current_dir(&request.current_dir)
.envs(&request.environment)
.stdin(Stdio::null());
command
}