diff --git a/Cargo.toml b/Cargo.toml index 5d63405..5456051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,16 @@ [package] -name = "dirwatcher" -version = "0.4.0" +name = "watchexec" +version = "0.7.0" authors = ["Matt Green "] +[profile.release] +lto = true + [dependencies] -notify = "2.6.3" libc = "0.2.16" +notify = "2.6.3" + +[dependencies.clap] +version = "2.12.1" +default-features = false +features = ["wrap_help"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00ffc76 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +debug: src/* Cargo.toml + @cargo build + +release: src/* Cargo.toml + @cargo build --release + +clean: + @cargo clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b7d4bd --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +#watchexec + +Software development often involves running the same commands over and over. Boring! + +`watchexec` is a **simple**, standalone tool that watches a path and runs a command whenever it detects modifications. + +Example use cases: + +* Automatically run unit tests +* Run linters/syntax checkers + +##Status + +Beta: CLI arguments subject to change + +##Features + +* Simple invocation and use +* Runs on OS X, Linux and Windows +* Monitors path specified on command line for changes + * Uses most efficient event polling mechanism, based on platform (except for [BSD](https://github.com/passcod/rsnotify#todo)) +* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving +* Optionally clears screen between executions +* Does not require a language runtime +* Small (~100 LOC) + +##Anti-Features + +* Not tied to any particular language or ecosystem +* Does not require a cryptic command line involving `xargs` + +##Usage + +Call `make test` when there are any changes in the `src` directory: + + $ watchexec src "make test" + +Note the use of quotes on the command. + +##Installation + +For now, clone the repo and `make release`. Copy the built executable from `target/release/watchexec` to somewhere in your path. + +##Credits + +* [notify](https://github.com/passcod/rsnotify) for doing most of the heavy-lifting \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d0184e1..85deafe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,80 +1,58 @@ -extern crate notify; +extern crate clap; extern crate libc; +extern crate notify; -use std::env; -use libc::system; -use notify::{Event, RecommendedWatcher, Watcher}; use std::ffi::CString; -use std::fs::File; -use std::io::BufRead; -use std::io::BufReader; -use std::path::{Path,PathBuf}; -use std::string::String; +use std::path::Path; use std::sync::mpsc::{channel, Receiver, RecvError}; use std::{thread, time}; +use std::process::Command; + +use libc::system; +use clap::{App, Arg}; +use notify::{Event, RecommendedWatcher, Watcher}; fn clear() { - let s = CString::new("clear").unwrap(); - unsafe { - system(s.as_ptr()); + // TODO: determine better way to do this + let clear_cmd; + if cfg!(target_os = "windows") { + clear_cmd = "cls"; + } + else { + clear_cmd = "clear"; } + let _ = Command::new(clear_cmd).status(); } -fn ignored(relpath: &Path, ignores: &Vec) -> bool { - for i in ignores.iter() { - if relpath.to_str().unwrap().starts_with(i) { - //println!("Ignoring {} because {}", relpath.to_str().unwrap(), i); - return true; - } - } - - false -} - -fn invoke(cmd: &String) { +fn invoke(cmd: &str) { + // TODO: determine a better way to get at system() let s = CString::new(cmd.clone()).unwrap(); unsafe { system(s.as_ptr()); } } -fn read_gitignore(path: &str) -> Result, std::io::Error> { - let f = try!(File::open(path)); - let reader = BufReader::new(f); - - let mut entries = vec![]; - for line in reader.lines() { - let l = try!(line).trim().to_string(); - - if l.starts_with("#") || l.len() == 0 { - continue; - } - - //println!("Read {}", l); - entries.push(l); - } - - Ok(entries) +fn ignored(_: &Path) -> bool { + // TODO: ignore *.pyc files + // TODO: handle .git directory? + false } -fn wait(rx: &Receiver, cwd: &PathBuf, ignore: &Vec) -> Result { +fn wait(rx: &Receiver) -> Result { loop { + // Block on initial notification let e = try!(rx.recv()); - - let ignored = match e.path { - Some(ref path) => { - let stripped = path.strip_prefix(cwd).unwrap(); - ignored(stripped, &ignore) - }, - None => false - }; - - if ignored { - continue; + if let Some(ref path) = e.path { + if ignored(&path) { + continue; + } } + // Accumulate subsequent events thread::sleep(time::Duration::from_millis(250)); + + // Drain rx buffer and drop them loop { match rx.try_recv() { Ok(_) => continue, @@ -87,28 +65,49 @@ fn wait(rx: &Receiver, cwd: &PathBuf, ignore: &Vec) -> Result ignored.extend(gitignores), - Err(_) => () - } + let args = App::new("watchexec") + .version("0.7") + .about("Runs a command when any of the specified files/directories are modified") + .arg(Arg::with_name("path") + .help("Path to watch for changes") + .required(true)) + .arg(Arg::with_name("command") + .help("Command to run") + .required(true)) + .arg(Arg::with_name("clear") + .help("Clear screen before running command") + .short("c") + .long("clear")) + .arg(Arg::with_name("debug") + .help("Enable debug messages") + .short("d") + .long("debug")) + .get_matches(); let (tx, rx) = channel(); - let mut watcher: RecommendedWatcher = Watcher::new(tx) - .expect("unable to create watcher"); - watcher.watch(".") - .expect("unable to start watching directory"); + let mut watcher: RecommendedWatcher = Watcher::new(tx).expect("unable to create watcher"); + + // TODO: handle multiple paths + let paths = args.values_of("path").unwrap(); + for path in paths { + watcher.watch(path).expect("unable to watch path"); + } + + let cmd = args.value_of("command").unwrap(); + let need_clear = args.is_present("clear"); + let debug = args.is_present("debug"); loop { - //clear(); - let e = wait(&rx, &cwd, &ignored) - .expect("error when waiting for filesystem changes"); + let e = wait(&rx).expect("error when waiting for filesystem changes"); - //println!("{:?} {:?}", e.op, e.path); - invoke(&cmd); + if need_clear { + clear(); + } + + if debug { + println!("*** {:?}: {:?}", e.op, e.path); + } + + invoke(cmd); } }