diff --git a/Cargo.toml b/Cargo.toml index 3c963be..1cc6837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runwhen" -version = "0.0.1" +version = "0.0.2" authors = ["Jeremy Wall "] description = "Runs a command on user specified triggers." repository = "https://github.com/zaphar/runwhen" @@ -12,4 +12,3 @@ license = "Apache-2.0" clap = "~2.19.0" humantime = "~1.0.0" notify = "~3.0.0" -subprocess = "~0.1.7" diff --git a/README.md b/README.md index f7e8937..0b1d3ea 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ Runs a command on user defined triggers. USAGE: - runwhen --cmd [SUBCOMMAND] + runwhen [OPTIONS] --cmd [SUBCOMMAND] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - -c, --cmd Command to run on supplied triggers + -c, --cmd Command to run on supplied triggers + -e, --env ... Command to run on supplied triggers SUBCOMMANDS: help Prints this message or the help of the given subcommand(s) diff --git a/src/error.rs b/src/error.rs index a51a534..ffa4b05 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,8 +22,8 @@ pub struct CommandError { } impl CommandError { - pub fn new(msg: String) -> CommandError { - CommandError { msg: msg } + pub fn new>(msg: S) -> CommandError { + CommandError { msg: msg.into() } } } diff --git a/src/exec.rs b/src/exec.rs index 5956127..bbd609c 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -14,19 +14,53 @@ use std::thread; use std::time::Duration; -use subprocess::{Exec, PopenError, ExitStatus}; +use std::process::{Command, Stdio}; use traits::Process; use error::CommandError; -pub fn run_cmd(cmd: &str) -> Result<(), PopenError> { - Exec::shell(cmd).join()?; - Ok(()) + +fn env_var_to_tuple(var: &str) -> (String, String) { + let mut vs = var.split('='); + if let Some(name) = vs.next() { + return match vs.next() { + Some(val) => (String::from(name), String::from(val)), + None => (String::from(name), "".to_string()), + } + } + ("".to_string(), "".to_string()) } -fn is_cmd_success(cmd: &str) -> bool { - match Exec::shell(cmd).join() { - Ok(ExitStatus::Exited(code)) => code == 0, +pub fn run_cmd(cmd: &str, env: &Option>) -> Result { + let args = cmd.split(' ').filter(|s| !s.is_empty()).collect::>(); + if args.len() < 1 { + return Err(CommandError::new("Empty command string passed in")); + } + let mut exec = Command::new(args[0]); + if args.len() > 1 { + exec.args(&args[1..]); + } + exec.stdout(Stdio::inherit()); + exec.stderr(Stdio::inherit()); + if let &Some(ref env_vars) = env { + for var in env_vars { + let tpl = env_var_to_tuple(var); + exec.env(tpl.0, tpl.1); + } + } + return match exec.output() { + Ok(out) => match out.status.code() { + Some(val) => Ok(val), + None => Ok(0), + }, + // TODO(jeremy): We should not swallow this error. + Err(_) => Err(CommandError::new("Error running command")), + } +} + +fn is_cmd_success(cmd: &str, env: Option>) -> bool { + match run_cmd(cmd, &env) { + Ok(code) => code == 0, _ => false, } } @@ -34,14 +68,19 @@ fn is_cmd_success(cmd: &str) -> bool { pub struct ExecProcess<'a> { test_cmd: &'a str, cmd: &'a str, + env: Option>, poll: Duration, } impl<'a> ExecProcess<'a> { - pub fn new(test_cmd: &'a str, cmd: &'a str, poll: Duration) -> ExecProcess<'a> { + pub fn new(test_cmd: &'a str, + cmd: &'a str, + env: Option>, + poll: Duration) -> ExecProcess<'a> { ExecProcess { test_cmd: test_cmd, cmd: cmd, + env: env, poll: poll, } } @@ -50,8 +89,9 @@ impl<'a> ExecProcess<'a> { impl<'a> Process for ExecProcess<'a> { fn run(&self) -> Result<(), CommandError> { loop { - if is_cmd_success(self.test_cmd) { - if let Err(err) = run_cmd(self.cmd) { + // TODO(jwall): Should we set the environment the same as the other command? + if is_cmd_success(self.test_cmd, None) { + if let Err(err) = run_cmd(self.cmd, &self.env) { println!("{:?}", err) } } diff --git a/src/file.rs b/src/file.rs index 27bb988..f38ec63 100644 --- a/src/file.rs +++ b/src/file.rs @@ -26,6 +26,7 @@ use exec::run_cmd; pub struct FileProcess<'a> { cmd: &'a str, + env: Option>, file: &'a str, method: WatchEventType, poll: Duration, @@ -33,12 +34,14 @@ pub struct FileProcess<'a> { impl<'a> FileProcess<'a> { pub fn new(cmd: &'a str, + env: Option>, file: &'a str, method: WatchEventType, poll: Duration) -> FileProcess<'a> { FileProcess { cmd: cmd, + env: env, file: file, method: method, poll: poll, @@ -46,8 +49,20 @@ impl<'a> FileProcess<'a> { } } -fn spawn_runner_thread(lock: Arc>, cmd: String, poll: Duration) { +fn spawn_runner_thread(lock: Arc>, cmd: String, + env: Option>, poll: Duration) { + let copied_env = env.and_then(|v| Some(v.iter().cloned().map(|s| String::from(s)).collect::>())); thread::spawn(move || { + let copied_env_refs: Option> = match copied_env { + Some(ref vec) => { + let mut refs: Vec<&str> = Vec::new(); + for s in vec.iter() { + refs.push(s); + } + Some(refs) + }, + None => None, + }; loop { // Wait our requisit number of seconds thread::sleep(poll); @@ -59,7 +74,7 @@ fn spawn_runner_thread(lock: Arc>, cmd: String, poll: Duration) { *signal = false; // Run our command! println!("exec: {}", cmd); - if let Err(err) = run_cmd(&cmd) { + if let Err(err) = run_cmd(&cmd, &copied_env_refs) { println!("{:?}", err) } }, @@ -123,7 +138,7 @@ impl<'a> Process for FileProcess<'a> { // TODO(jeremy): Is this sufficent or do we want to ignore // any events that come in while the command is running? let lock = Arc::new(Mutex::new(false)); - spawn_runner_thread(lock.clone(), self.cmd.to_string(), self.poll); + spawn_runner_thread(lock.clone(), self.cmd.to_string(), self.env.clone(), self.poll); wait_for_fs_events(lock, self.method.clone(), self.file) } } diff --git a/src/main.rs b/src/main.rs index 5061085..6015f2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ extern crate clap; extern crate humantime; extern crate notify; -extern crate subprocess; use std::process; use std::str::FromStr; @@ -41,6 +40,7 @@ fn do_flags<'a>() -> clap::ArgMatches<'a> { (author: crate_authors!()) (about: "Runs a command on user defined triggers.") (@arg cmd: -c --cmd +required +takes_value "Command to run on supplied triggers") + (@arg env: -e --env +takes_value ... "Command to run on supplied triggers") (@subcommand watch => (about: "Trigger that fires when a file or directory changes.") // TODO(jeremy): We need to support filters @@ -73,6 +73,14 @@ fn main() { let app = do_flags(); // Unwrap because this flag is required. let cmd = app.value_of("cmd").expect("cmd flag is required"); + let mut maybe_env = None; + if let Some(env_values) = app.values_of("env") { + let mut env_vec = Vec::new(); + for v in env_values { + env_vec.push(v); + } + maybe_env = Some(env_vec); + } let mut process: Option> = None; if let Some(matches) = app.subcommand_matches("watch") { // Unwrap because this flag is required. @@ -83,7 +91,8 @@ fn main() { } let poll = matches.value_of("poll").unwrap_or("5s"); let dur = humantime::parse_duration(poll).expect("Invalid poll value."); - process = Some(Box::new(FileProcess::new(cmd, file, method, dur))); + process = Some(Box::new(FileProcess::new( + cmd, maybe_env, file, method, dur))); } else if let Some(matches) = app.subcommand_matches("timer") { // Unwrap because this flag is required. let dur = humantime::parse_duration(matches.value_of("duration") @@ -102,7 +111,8 @@ fn main() { } else { None }; - process = Some(Box::new(TimerProcess::new(cmd, duration, max_repeat))); + process = Some(Box::new(TimerProcess::new( + cmd, maybe_env, duration, max_repeat))); } Err(msg) => { println!("Malformed duration {:?}", msg); @@ -114,7 +124,7 @@ fn main() { let ifcmd = matches.value_of("ifcmd").expect("ifcmd flag is required"); let dur = humantime::parse_duration(matches.value_of("poll").unwrap_or("5s")); process = match dur { - Ok(duration) => Some(Box::new(ExecProcess::new(ifcmd, cmd, duration))), + Ok(duration) => Some(Box::new(ExecProcess::new(ifcmd, cmd, maybe_env, duration))), Err(msg) => { println!("Malformed poll {:?}", msg); process::exit(1) diff --git a/src/timer.rs b/src/timer.rs index 462da3f..1163e8d 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -20,14 +20,19 @@ use exec::run_cmd; pub struct TimerProcess<'a> { cmd: &'a str, + env: Option>, poll_duration: Duration, max_repeat: Option, } impl<'a> TimerProcess<'a> { - pub fn new(cmd: &'a str, poll_duration: Duration, max_repeat: Option) -> TimerProcess<'a> { + pub fn new(cmd: &'a str, + env: Option>, + poll_duration: Duration, + max_repeat: Option) -> TimerProcess<'a> { TimerProcess { cmd: cmd, + env: env, poll_duration: poll_duration, max_repeat: max_repeat, } @@ -41,7 +46,7 @@ impl<'a> Process for TimerProcess<'a> { if self.max_repeat.is_some() && counter >= self.max_repeat.unwrap() { return Ok(()); } - if let Err(err) = run_cmd(self.cmd) { + if let Err(err) = run_cmd(self.cmd, &self.env) { println!("{:?}", err) } thread::sleep(self.poll_duration);