Rename to watchexec

This commit is contained in:
Matt Green 2016-09-18 16:42:11 -04:00
parent 5b97663990
commit e01c45d6f5
4 changed files with 135 additions and 74 deletions

View File

@ -1,8 +1,16 @@
[package] [package]
name = "dirwatcher" name = "watchexec"
version = "0.4.0" version = "0.7.0"
authors = ["Matt Green <mattgreenrocks@gmail.com>"] authors = ["Matt Green <mattgreenrocks@gmail.com>"]
[profile.release]
lto = true
[dependencies] [dependencies]
notify = "2.6.3"
libc = "0.2.16" libc = "0.2.16"
notify = "2.6.3"
[dependencies.clap]
version = "2.12.1"
default-features = false
features = ["wrap_help"]

8
Makefile Normal file
View File

@ -0,0 +1,8 @@
debug: src/* Cargo.toml
@cargo build
release: src/* Cargo.toml
@cargo build --release
clean:
@cargo clean

46
README.md Normal file
View File

@ -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

View File

@ -1,80 +1,58 @@
extern crate notify; extern crate clap;
extern crate libc; extern crate libc;
extern crate notify;
use std::env;
use libc::system;
use notify::{Event, RecommendedWatcher, Watcher};
use std::ffi::CString; use std::ffi::CString;
use std::fs::File; use std::path::Path;
use std::io::BufRead;
use std::io::BufReader;
use std::path::{Path,PathBuf};
use std::string::String;
use std::sync::mpsc::{channel, Receiver, RecvError}; use std::sync::mpsc::{channel, Receiver, RecvError};
use std::{thread, time}; use std::{thread, time};
use std::process::Command;
use libc::system;
use clap::{App, Arg};
use notify::{Event, RecommendedWatcher, Watcher};
fn clear() { fn clear() {
let s = CString::new("clear").unwrap(); // TODO: determine better way to do this
unsafe { let clear_cmd;
system(s.as_ptr()); if cfg!(target_os = "windows") {
clear_cmd = "cls";
}
else {
clear_cmd = "clear";
} }
let _ = Command::new(clear_cmd).status();
} }
fn ignored(relpath: &Path, ignores: &Vec<String>) -> bool { fn invoke(cmd: &str) {
for i in ignores.iter() { // TODO: determine a better way to get at system()
if relpath.to_str().unwrap().starts_with(i) {
//println!("Ignoring {} because {}", relpath.to_str().unwrap(), i);
return true;
}
}
false
}
fn invoke(cmd: &String) {
let s = CString::new(cmd.clone()).unwrap(); let s = CString::new(cmd.clone()).unwrap();
unsafe { unsafe {
system(s.as_ptr()); system(s.as_ptr());
} }
} }
fn read_gitignore(path: &str) -> Result<Vec<String>, std::io::Error> { fn ignored(_: &Path) -> bool {
let f = try!(File::open(path)); // TODO: ignore *.pyc files
let reader = BufReader::new(f); // TODO: handle .git directory?
false
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 wait(rx: &Receiver<Event>, cwd: &PathBuf, ignore: &Vec<String>) -> Result<Event, RecvError> { fn wait(rx: &Receiver<Event>) -> Result<Event, RecvError> {
loop { loop {
// Block on initial notification
let e = try!(rx.recv()); let e = try!(rx.recv());
if let Some(ref path) = e.path {
let ignored = match e.path { if ignored(&path) {
Some(ref path) => { continue;
let stripped = path.strip_prefix(cwd).unwrap(); }
ignored(stripped, &ignore)
},
None => false
};
if ignored {
continue;
} }
// Accumulate subsequent events
thread::sleep(time::Duration::from_millis(250)); thread::sleep(time::Duration::from_millis(250));
// Drain rx buffer and drop them
loop { loop {
match rx.try_recv() { match rx.try_recv() {
Ok(_) => continue, Ok(_) => continue,
@ -87,28 +65,49 @@ fn wait(rx: &Receiver<Event>, cwd: &PathBuf, ignore: &Vec<String>) -> Result<Eve
} }
fn main() { fn main() {
let cmd = env::args().nth(1).expect("Argument 1 needs to be a command"); let args = App::new("watchexec")
let cwd = env::current_dir().unwrap(); .version("0.7")
.about("Runs a command when any of the specified files/directories are modified")
let mut ignored = vec![]; .arg(Arg::with_name("path")
ignored.push(String::from(".")); .help("Path to watch for changes")
match read_gitignore(".gitignore") { .required(true))
Ok(gitignores) => ignored.extend(gitignores), .arg(Arg::with_name("command")
Err(_) => () .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 (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx) let mut watcher: RecommendedWatcher = Watcher::new(tx).expect("unable to create watcher");
.expect("unable to create watcher");
watcher.watch(".") // TODO: handle multiple paths
.expect("unable to start watching directory"); 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 { loop {
//clear(); let e = wait(&rx).expect("error when waiting for filesystem changes");
let e = wait(&rx, &cwd, &ignored)
.expect("error when waiting for filesystem changes");
//println!("{:?} {:?}", e.op, e.path); if need_clear {
invoke(&cmd); clear();
}
if debug {
println!("*** {:?}: {:?}", e.op, e.path);
}
invoke(cmd);
} }
} }