use command::output_and_write_streams;
use regex::Regex;
use std::ffi::OsString;
use std::fmt::Display;
use std::io::Write;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Output;
use std::sync::LazyLock;
#[cfg(feature = "which_problem")]
use which_problem::Which;
mod command;
pub trait CommandWithName {
fn name(&mut self) -> String;
fn mut_cmd(&mut self) -> &mut Command;
fn named(&mut self, s: impl AsRef<str>) -> NamedCommand<'_> {
let name = s.as_ref().to_string();
let command = self.mut_cmd();
NamedCommand { name, command }
}
#[allow(clippy::needless_lifetimes)]
fn named_fn<'a>(&'a mut self, f: impl FnOnce(&mut Command) -> String) -> NamedCommand<'a> {
let cmd = self.mut_cmd();
let name = f(cmd);
self.named(name)
}
fn named_output(&mut self) -> Result<NamedOutput, CmdError> {
let name = self.name();
self.mut_cmd()
.output()
.map_err(|io_error| CmdError::SystemError(name.clone(), io_error))
.map(|output| NamedOutput {
name: name.clone(),
output,
})
.and_then(NamedOutput::nonzero_captured)
}
fn stream_output<OW, EW>(
&mut self,
stdout_write: OW,
stderr_write: EW,
) -> Result<NamedOutput, CmdError>
where
OW: Write + Send,
EW: Write + Send,
{
let name = &self.name();
let cmd = self.mut_cmd();
output_and_write_streams(cmd, stdout_write, stderr_write)
.map_err(|io_error| CmdError::SystemError(name.clone(), io_error))
.map(|output| NamedOutput {
name: name.clone(),
output,
})
.and_then(NamedOutput::nonzero_streamed)
}
}
impl CommandWithName for Command {
fn name(&mut self) -> String {
crate::display(self)
}
fn mut_cmd(&mut self) -> &mut Command {
self
}
}
impl CommandWithName for &mut Command {
fn name(&mut self) -> String {
crate::display(self)
}
fn mut_cmd(&mut self) -> &mut Command {
self
}
}
pub struct NamedCommand<'a> {
name: String,
command: &'a mut Command,
}
impl<'a> From<&'a mut Command> for NamedCommand<'a> {
fn from(command: &'a mut Command) -> Self {
NamedCommand {
name: command.name(),
command,
}
}
}
impl CommandWithName for NamedCommand<'_> {
fn name(&mut self) -> String {
self.name.to_string()
}
fn mut_cmd(&mut self) -> &mut Command {
self.command
}
}
impl CommandWithName for &mut NamedCommand<'_> {
fn name(&mut self) -> String {
self.name.to_string()
}
fn mut_cmd(&mut self) -> &mut Command {
self.command
}
}
pub trait OutputWithName {
#[must_use]
fn named(self, s: impl AsRef<str>) -> NamedOutput;
}
impl OutputWithName for Output {
fn named(self, s: impl AsRef<str>) -> NamedOutput {
NamedOutput {
name: s.as_ref().to_string(),
output: self,
}
}
}
pub trait ExitStatusFromCode {
#[cfg(unix)]
#[must_use]
fn from_code(code: u8) -> ExitStatus;
#[cfg(windows)]
#[must_use]
fn from_code(code: u32) -> ExitStatus;
}
impl ExitStatusFromCode for ExitStatus {
#[cfg(unix)]
fn from_code(code: u8) -> ExitStatus {
status_from_code(code.into())
}
#[cfg(windows)]
fn from_code(code: u32) -> ExitStatus {
status_from_code(code)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamedOutput {
name: String,
output: Output,
}
impl NamedOutput {
pub fn nonzero_captured(self) -> Result<NamedOutput, CmdError> {
nonzero_captured(self.name, self.output)
}
pub fn nonzero_streamed(self) -> Result<NamedOutput, CmdError> {
nonzero_streamed(self.name, self.output)
}
#[must_use]
pub fn status(&self) -> &ExitStatus {
&self.output.status
}
#[must_use]
pub fn stdout(&self) -> &Vec<u8> {
&self.output.stdout
}
#[must_use]
pub fn stderr(&self) -> &Vec<u8> {
&self.output.stderr
}
#[must_use]
pub fn stdout_lossy(&self) -> String {
String::from_utf8_lossy(&self.output.stdout).to_string()
}
#[must_use]
pub fn stderr_lossy(&self) -> String {
String::from_utf8_lossy(&self.output.stderr).to_string()
}
#[must_use]
pub fn name(&self) -> String {
self.name.clone()
}
#[must_use]
pub fn output(&self) -> &Output {
&self.output
}
}
impl AsRef<Output> for NamedOutput {
fn as_ref(&self) -> &Output {
&self.output
}
}
impl<'a> From<&'a NamedOutput> for &'a Output {
fn from(value: &'a NamedOutput) -> Self {
&value.output
}
}
impl From<NamedOutput> for Output {
fn from(value: NamedOutput) -> Self {
value.output
}
}
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(std::ffi::OsStr::to_string_lossy)
.map(|arg| {
if QUOTE_ARG_RE.is_match(&arg) {
format!("{arg:?}")
} else {
format!("{arg}")
}
}),
)
.collect::<Vec<String>>()
.join(" ")
}
#[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>,
{
let env = env
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect::<std::collections::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([display(cmd)])
.collect::<Vec<String>>()
.join(" ")
}
#[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub enum CmdError {
SystemError(String, std::io::Error),
NonZeroExitNotStreamed(NamedOutput),
NonZeroExitAlreadyStreamed(NamedOutput),
}
fn bashify(status: &ExitStatus) -> i32 {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status
.code()
.or_else(|| status.signal().map(|s| 128 + s))
.unwrap_or(1)
}
#[cfg(windows)]
{
status.code().unwrap_or(1)
}
}
fn signal_line(status: &ExitStatus) -> Option<String> {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.signal().map(|signal| {
let name = match signal {
1 => "SIGHUP",
2 => "SIGINT",
3 => "SIGQUIT",
6 => "SIGABRT",
9 => "SIGKILL",
14 => "SIGALRM",
15 => "SIGTERM",
_ => "",
};
if name.is_empty() {
format!("signal: {signal}")
} else {
format!("signal: {signal} ({name})")
}
})
}
#[cfg(windows)]
{
let _ = status;
None
}
}
impl Display for CmdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CmdError::SystemError(name, error) => {
write!(f, "Could not run command `{name}`. {error}")
}
CmdError::NonZeroExitNotStreamed(named_output) => {
let stdout = display_out_or_empty(&named_output.output.stdout);
let stderr = display_out_or_empty(&named_output.output.stderr);
writeln!(f, "Command failed `{name}`", name = named_output.name())?;
writeln!(
f,
"exit status: {status}",
status = bashify(&named_output.output.status)
)?;
if let Some(signal_line) = signal_line(&named_output.output.status) {
writeln!(f, "{signal_line}")?;
}
writeln!(f, "stdout: {stdout}",)?;
write!(f, "stderr: {stderr}",)
}
CmdError::NonZeroExitAlreadyStreamed(named_output) => {
writeln!(f, "Command failed `{name}`", name = named_output.name())?;
writeln!(
f,
"exit status: {status}",
status = bashify(&named_output.output.status)
)?;
if let Some(signal_line) = signal_line(&named_output.output.status) {
writeln!(f, "{signal_line}")?;
}
writeln!(f, "stdout: <see above>")?;
write!(f, "stderr: <see above>")
}
}
}
}
impl std::error::Error for CmdError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CmdError::SystemError(_, io_err) => Some(io_err),
CmdError::NonZeroExitNotStreamed(_) | CmdError::NonZeroExitAlreadyStreamed(_) => None,
}
}
}
impl CmdError {
#[must_use]
pub fn name(&self) -> std::borrow::Cow<'_, str> {
match self {
CmdError::SystemError(name, _) => name.into(),
CmdError::NonZeroExitNotStreamed(out) | CmdError::NonZeroExitAlreadyStreamed(out) => {
out.name.as_str().into()
}
}
}
pub fn status(&self) -> ExitStatus {
match self {
CmdError::SystemError(_, error) => status_from_error(error),
CmdError::NonZeroExitNotStreamed(named_output) => named_output.status().to_owned(),
CmdError::NonZeroExitAlreadyStreamed(named_output) => named_output.status().to_owned(),
}
}
}
impl From<CmdError> for NamedOutput {
fn from(value: CmdError) -> Self {
match value {
CmdError::SystemError(name, error) => NamedOutput {
name,
output: Output {
status: status_from_error(&error),
stdout: Vec::new(),
stderr: error.to_string().into_bytes(),
},
},
CmdError::NonZeroExitNotStreamed(named)
| CmdError::NonZeroExitAlreadyStreamed(named) => named,
}
}
}
#[cfg(not(any(unix, windows)))]
compile_error!(
"fun_run constructs `ExitStatus` values and only supports `unix` and `windows` targets"
);
fn status_from_code(code: u32) -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(((code & 0xff) as i32) << 8)
}
#[cfg(windows)]
{
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(code)
}
}
fn status_from_error(error: &std::io::Error) -> ExitStatus {
use std::io::ErrorKind;
let code = match error.kind() {
ErrorKind::NotFound => 127, ErrorKind::PermissionDenied | ErrorKind::IsADirectory | ErrorKind::NotADirectory | ErrorKind::ArgumentListTooLong | ErrorKind::OutOfMemory | ErrorKind::ExecutableFileBusy => 126,
_ => 1,
};
status_from_code(code)
}
fn display_out_or_empty(contents: &[u8]) -> String {
let contents = String::from_utf8_lossy(contents);
if contents.trim().is_empty() {
"<empty>".to_string()
} else {
contents.to_string()
}
}
#[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(NamedOutput { name, output })
} else {
Err(CmdError::NonZeroExitAlreadyStreamed(NamedOutput {
name,
output,
}))
}
}
pub fn nonzero_captured(name: String, output: impl Into<Output>) -> Result<NamedOutput, CmdError> {
let output = output.into();
if output.status.success() {
Ok(NamedOutput { name, output })
} else {
Err(CmdError::NonZeroExitNotStreamed(NamedOutput {
name,
output,
}))
}
}
#[cfg(feature = "which_problem")]
pub fn map_which_problem(
error: CmdError,
cmd: &mut Command,
path_env: Option<OsString>,
) -> CmdError {
match error {
CmdError::SystemError(name, error) => {
CmdError::SystemError(name, annotate_which_problem(error, cmd, path_env))
}
CmdError::NonZeroExitNotStreamed(_) | CmdError::NonZeroExitAlreadyStreamed(_) => error,
}
}
#[must_use]
#[cfg(feature = "which_problem")]
fn annotate_which_problem(
error: std::io::Error,
cmd: &mut Command,
path_env: Option<OsString>,
) -> std::io::Error {
let program = cmd.get_program().to_os_string();
let current_working_dir = cmd.get_current_dir().map(std::path::Path::to_path_buf);
let problem = Which {
cwd: current_working_dir,
program,
path_env,
..Which::default()
}
.diagnose();
let annotation = match problem {
Ok(details) => format!("\nSystem diagnostic information:\n\n{details}"),
Err(error) => {
format!("\nInternal error while gathering diagnostic information:\n\n{error}")
}
};
annotate_io_error(error, annotation)
}
#[must_use]
#[cfg(feature = "which_problem")]
fn annotate_io_error(source: std::io::Error, annotation: String) -> std::io::Error {
IoErrorAnnotation::new(source, annotation).into_io_error()
}
#[derive(Debug)]
#[cfg(feature = "which_problem")]
pub(crate) struct IoErrorAnnotation {
source: std::io::Error,
annotation: String,
}
#[cfg(feature = "which_problem")]
impl IoErrorAnnotation {
pub(crate) fn new(source: std::io::Error, annotation: String) -> Self {
Self { source, annotation }
}
pub(crate) fn into_io_error(self) -> std::io::Error {
std::io::Error::new(self.source.kind(), self)
}
}
#[cfg(feature = "which_problem")]
impl std::fmt::Display for IoErrorAnnotation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.source)?;
f.write_str(&self.annotation)?;
Ok(())
}
}
#[cfg(feature = "which_problem")]
impl std::error::Error for IoErrorAnnotation {
fn cause(&self) -> Option<&dyn std::error::Error> {
self.source()
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_from_code() {
for code in 0..=255 {
let status = ExitStatus::from_code(code);
assert_eq!(Some(code as i32), status.code());
}
}
#[test]
fn maps_error_kinds_to_shell_codes() {
use std::io::{Error, ErrorKind};
assert_eq!(
Some(127),
status_from_error(&Error::from(ErrorKind::NotFound)).code()
);
assert_eq!(
Some(126),
status_from_error(&Error::from(ErrorKind::PermissionDenied)).code()
);
assert_eq!(
Some(1),
status_from_error(&Error::from(ErrorKind::Other)).code()
);
}
#[test]
#[cfg(unix)]
fn maps_signal_to_exit() {
use std::os::unix::process::ExitStatusExt;
let sigterm = 15;
let status = ExitStatus::from_raw(sigterm);
assert_eq!(None, status.code());
assert_eq!(Some(15), status.signal());
let code = bashify(&status);
assert_eq!(143, code);
}
#[test]
#[cfg(unix)]
fn maps_signal_to_human_readable_output() {
use std::os::unix::process::ExitStatusExt;
let sigterm = 15;
let status = ExitStatus::from_raw(sigterm);
assert_eq!(None, status.code());
assert_eq!(Some(15), status.signal());
assert_eq!(
String::from("signal: 15 (SIGTERM)"),
signal_line(&status).unwrap()
);
let signal = 126;
let status = ExitStatus::from_raw(signal);
assert_eq!(None, status.code());
assert_eq!(Some(126), status.signal());
assert_eq!(String::from("signal: 126"), signal_line(&status).unwrap());
}
}