use tracing::{debug, error, info};
mod macros;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
ExitCode(i32),
Signal(i32),
NoExitCodeAndSignal,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "I/O error: {e}"),
Error::ExitCode(exit_code) => write!(f, "Exit code: {exit_code}"),
Error::Signal(signal) => write!(f, "Signal: {signal}"),
Error::NoExitCodeAndSignal => write!(f, "No exit code and signal"),
}
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
struct Metadata<'a> {
env_key: &'a str,
program: &'a str,
args: &'a [&'a str],
}
#[cfg(windows)]
static DEFAULT_METADATA: Metadata = Metadata {
env_key: "COMSPEC",
program: "cmd.exe",
args: &["/D", "/S", "/C"],
};
#[cfg(unix)]
static DEFAULT_METADATA: Metadata = Metadata {
env_key: "SHELL",
program: "/bin/sh",
args: &["-c"],
};
fn parse_program() -> String {
std::env::var(DEFAULT_METADATA.env_key).unwrap_or_else(|e| {
debug!(
default_program = DEFAULT_METADATA.program,
env_key = DEFAULT_METADATA.env_key,
error = ?e,
"Failed to get shell environment variable, falling back to default program."
);
DEFAULT_METADATA.program.to_string()
})
}
#[derive(Debug)]
pub struct Sheller {
program: String,
args: Vec<&'static str>,
script: String,
}
impl Default for Sheller {
fn default() -> Self {
Self {
program: parse_program(),
args: DEFAULT_METADATA.args.into(),
script: String::new(),
}
}
}
impl Sheller {
#[must_use]
pub fn new<T>(script: T) -> Self
where
T: Into<String>,
{
Self {
script: script.into(),
..Default::default()
}
}
#[must_use]
pub fn build(self) -> std::process::Command {
let mut command = std::process::Command::new(&self.program);
command.args(&self.args);
command.arg(self.script);
command
}
pub fn run(self) {
self.build().run();
}
pub fn try_run(self) -> Result<()> {
self.build().try_run()
}
}
pub trait CommandExt {
fn run(&mut self);
fn try_run(&mut self) -> Result<()>;
}
#[cfg(unix)]
fn get_signal(a: std::process::ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
a.signal()
}
#[cfg(windows)]
fn get_signal(_: std::process::ExitStatus) -> Option<i32> {
None
}
impl CommandExt for std::process::Command {
fn run(&mut self) {
self.try_run().unwrap();
}
fn try_run(&mut self) -> Result<()> {
info!(command = ?self, "Running command.");
let mut command = self.spawn().map_err(|e| {
error!(command = ?self, error = ?e, "Failed to spawn command.");
e
})?;
let status = command.wait().map_err(|e| {
error!(command = ?self, error = ?e, "Failed to wait for command.");
e
})?;
if let Some(exit_code) = status.code() {
if exit_code == 0 {
info!(command = ?self, "Succeeded to run command with zero exit code.");
Ok(())
} else {
error!(command = ?self, exit_code = ?exit_code, "Failed to run command with non-zero exit code.");
Err(Error::ExitCode(exit_code))
}
} else if let Some(signal) = get_signal(status) {
error!(command = ?self, signal = ?signal, "Failed to run command with signal.");
Err(Error::Signal(signal))
} else {
error!(command = ?self, "Failed to run command with no exit code and signal.");
Err(Error::NoExitCodeAndSignal)
}
}
}