diff --git a/libcitadel/src/exec.rs b/libcitadel/src/exec.rs new file mode 100644 index 0000000..02f8b55 --- /dev/null +++ b/libcitadel/src/exec.rs @@ -0,0 +1,162 @@ +use std::env; +use std::fs::File; +use std::io::{self,Seek,Read,BufReader,BufRead,SeekFrom}; +use std::path::{Path,PathBuf}; +use std::process::{Command,ExitStatus,Stdio}; + +use crate::Result; + +#[macro_export] +macro_rules! cmd { + ($cmd:expr, $e:expr) => { $crate::Exec::new($cmd).run(String::from($e)) }; + ($cmd:expr, $fmt:expr, $($arg:tt)+) => { $crate::Exec::new($cmd).run(format!($fmt, $($arg)+)) }; +} + +#[macro_export] +macro_rules! cmd_ok { + ($cmd:expr, $e:expr) => { $crate::Exec::new($cmd).run_ok(String::from($e)) }; + ($cmd:expr, $fmt:expr, $($arg:tt)+) => { $crate::Exec::new($cmd).run_ok(format!($fmt, $($arg)+)) }; +} + +#[macro_export] +macro_rules! cmd_with_output { + ($cmd:expr, $e:expr) => { $crate::Exec::new($cmd).output(String::from($e)) }; + ($cmd:expr, $fmt:expr, $($arg:tt)+) => { $crate::Exec::new($cmd).output(format!($fmt, $($arg)+)) }; +} + +pub struct Exec { + cmd_name: String, + cmd: Command, +} + +impl Exec { + pub fn new(cmd: impl AsRef) -> Exec { + Exec { + cmd_name: cmd.as_ref().to_string(), + cmd: Command::new(cmd.as_ref()), + } + } + + pub fn quiet(&mut self) -> &mut Self { + self.cmd + .stdout(Stdio::null()) + .stderr(Stdio::null()); + self + } + + pub fn run(&mut self, args: impl AsRef) -> Result<()> { + self.ensure_command_exists()?; + verbose!("cmd {} {}", self.cmd_name, args.as_ref()); + let args: Vec<&str> = args.as_ref().split_whitespace().collect(); + let result = self.cmd + .args(args) + .output()?; + + for line in BufReader::new(result.stderr.as_slice()).lines() { + verbose!(" {}", line?); + } + self.check_cmd_status(&result.status) + } + + + pub fn run_ok(&mut self, args: impl AsRef) -> Result { + self.ensure_command_exists()?; + let args: Vec<&str> = args.as_ref().split_whitespace().collect(); + let status = self.cmd + .args(args) + .status()?; + + Ok(status.success()) + } + + pub fn output(&mut self, args: impl AsRef) -> Result { + self.ensure_command_exists()?; + self.add_args(args.as_ref()); + let result = self.cmd.stderr(Stdio::inherit()).output()?; + self.check_cmd_status(&result.status)?; + Ok(String::from_utf8(result.stdout).unwrap().trim().to_owned()) + } + + /// + /// Execute a command, pipe the contents of a file to stdin, return the output as a `String` + /// + pub fn pipe_input(&mut self, args: S, input: P, range: FileRange) -> Result + where S: AsRef, P: AsRef + { + let mut r = ranged_reader(input.as_ref(), range)?; + self.ensure_command_exists()?; + self.add_args(args); + let mut child = self.cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + + let stdin = child.stdin.as_mut().unwrap(); + io::copy(&mut r, stdin)?; + let output = child.wait_with_output()?; + Ok(String::from_utf8(output.stdout).unwrap().trim().to_owned()) + } + + fn add_args(&mut self, args: impl AsRef) { + let args: Vec<&str> = args.as_ref().split_whitespace().collect(); + self.cmd.args(args); + } + + fn check_cmd_status(&self, status: &ExitStatus) -> Result<()> { + if !status.success() { + match status.code() { + Some(code) => bail!("command {} failed with exit code: {}", self.cmd_name, code), + None => bail!("command {} failed with no exit code", self.cmd_name), + } + } + Ok(()) + } + + fn ensure_command_exists(&self) -> Result<()> { + let path = Path::new(&self.cmd_name); + if !path.is_absolute() { + Exec::search_path(&self.cmd_name)?; + return Ok(()) + } else if path.exists() { + return Ok(()) + } + Err(format_err!("Cannot execute '{}': command does not exist", self.cmd_name)) + } + + fn search_path(filename: &str) -> Result { + let path_var = env::var("PATH")?; + for mut path in env::split_paths(&path_var) { + path.push(filename); + if path.exists() { + return Ok(path); + } + } + Err(format_err!("Could not find {} in $PATH", filename)) + } +} + +pub enum FileRange { + All, + Offset(usize), + Range{offset: usize, len: usize}, +} + + +fn ranged_reader>(path: P, range: FileRange) -> Result> { + let mut f = File::open(path.as_ref())?; + let offset = match range { + FileRange::All => 0, + FileRange::Offset(n) => n, + FileRange::Range {offset, len: _} => offset, + }; + if offset > 0 { + f.seek(SeekFrom::Start(offset as u64))?; + } + let r = BufReader::new(f); + if let FileRange::Range {offset: _, len} = range { + Ok(Box::new(r.take(len as u64))) + } else { + Ok(Box::new(r)) + } +}