diff --git a/.watch.toml b/.watch.toml index dbd333e..12dc615 100644 --- a/.watch.toml +++ b/.watch.toml @@ -4,6 +4,6 @@ name = "print hello" # Where to look for changes? path = "src" # What to execute on change? -command = "echo \"hello world\"" +command = "echo \"hello $EVENT_PATH\"" # Repeat this to watch multiple paths diff --git a/Cargo.lock b/Cargo.lock index 8d4fd8e..e6fcbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bitflags" version = "1.2.1" @@ -49,13 +55,14 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "caretaker" -version = "0.1.4" +version = "0.2.0" dependencies = [ "ansi_term 0.12.1", - "crossbeam-channel", + "crossbeam-channel 0.5.0", "custom_error", "glob", "notify", + "parking_lot", "serde", "structopt", "toml", @@ -68,14 +75,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] -name = "chashmap" -version = "2.2.2" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45" -dependencies = [ - "owning_ref", - "parking_lot", -] +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" @@ -92,13 +95,38 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "const_fn" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce90df4c658c62f12d78f7508cf92f9173e5184a539c10bfe54a3107b3ffd0f2" + [[package]] name = "crossbeam-channel" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.0", ] [[package]] @@ -107,16 +135,28 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" dependencies = [ - "autocfg", - "cfg-if", + "autocfg 0.1.7", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec91540d98355f690a86367e566ecad2e9e579f230230eb7c21398372be73ea5" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 1.0.0", + "const_fn", "lazy_static", ] [[package]] name = "custom_error" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a0fc65739ae998afc8d68e64bdac2efd1bc4ffa1a0703d171ef2defae3792f" +checksum = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12" [[package]] name = "filetime" @@ -124,7 +164,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff6d4dab0aa0c8e6346d46052e93b13a16cf847b54ed357087c35011048cc7d" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "redox_syscall", "winapi 0.3.8", @@ -149,12 +189,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -215,6 +249,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -248,24 +291,27 @@ checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" [[package]] name = "libc" -version = "0.2.66" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" [[package]] -name = "log" -version = "0.4.8" +name = "lock_api" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" dependencies = [ - "cfg-if", + "scopeguard", ] [[package]] -name = "maybe-uninit" -version = "2.0.0" +name = "log" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if 0.1.10", +] [[package]] name = "mio" @@ -273,7 +319,7 @@ version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "fuchsia-zircon", "fuchsia-zircon-sys", "iovec", @@ -316,21 +362,20 @@ version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", "winapi 0.3.8", ] [[package]] name = "notify" -version = "5.0.0-pre.2" +version = "5.0.0-pre.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b00c0b65188bffb5598c302e19b062feb94adef02c31f15622a163c95d673c3" +checksum = "77d03607cf88b4b160ba0e9ed425fff3cee3b55ac813f0c685b3a3772da37d0e" dependencies = [ "anymap", "bitflags", - "chashmap", - "crossbeam-channel", + "crossbeam-channel 0.4.0", "filetime", "fsevent", "fsevent-sys", @@ -342,68 +387,61 @@ dependencies = [ "winapi 0.3.8", ] -[[package]] -name = "owning_ref" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "parking_lot" -version = "0.4.8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" dependencies = [ - "owning_ref", + "instant", + "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.2.14" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "instant", "libc", - "rand", + "redox_syscall", "smallvec", "winapi 0.3.8", ] [[package]] name = "proc-macro-error" -version = "0.4.4" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c98547ceaea14eeb26fcadf51dc70d01a2479a7839170eae133721105e4428" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "rustversion", "syn", + "version_check", ] [[package]] name = "proc-macro-error-attr" -version = "0.4.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2bf5d493cf5d3e296beccfd61794e445e830dfc8070a9c248ad3ee071392c6c" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "rustversion", - "syn", - "syn-mid", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.7" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] @@ -417,60 +455,12 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.8", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -[[package]] -name = "rustversion" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0538bd897e17257b0128d2fd95c2ed6df939374073a36166051a79e2eb7986" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "same-file" version = "1.0.6" @@ -480,20 +470,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" -version = "1.0.104" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.104" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ "proc-macro2", "quote", @@ -508,18 +504,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" -dependencies = [ - "maybe-uninit", -] - -[[package]] -name = "stable_deref_trait" -version = "1.1.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" [[package]] name = "strsim" @@ -529,19 +516,20 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.7" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884ae79d6aad1e738f4a70dff314203fd498490a63ebc4d03ea83323c40b7b72" +checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8" dependencies = [ "clap", + "lazy_static", "structopt-derive", ] [[package]] name = "structopt-derive" -version = "0.4.0" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a97f829a34a0a9d5b353a881025a23b8c9fd09d46be6045df6b22920dbd7a93" +checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8" dependencies = [ "heck", "proc-macro-error", @@ -552,26 +540,15 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.13" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" +checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] -[[package]] -name = "syn-mid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd3937748a7eccff61ba5b90af1a20dbf610858923a9192ea0ecb0cb77db1d0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -583,9 +560,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d1404644c8b12b16bfcffa4322403a91a451584daaaa7c28d3152e6cbc98cf" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ "serde", ] @@ -614,6 +591,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "walkdir" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index bd45105..7873b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "caretaker" -version = "0.1.4" +version = "0.2.0" authors = ["Maroš Grego "] edition = "2018" description = "A simple, configurable filesystem watcher" @@ -12,19 +12,15 @@ license = "MIT" readme = "README.md" [dependencies] -toml = "^0.5.5" -notify = "^5.0.0-pre.2" -serde = { version = "^1.0.104", features = ["derive"] } -structopt = "^0.3.7" -crossbeam-channel = "0.4.0" +toml = "0.5.7" +notify = "^5.0.0-pre.3" +serde = { version = "1.0.117", features = ["derive"] } +structopt = "^0.3.20" +crossbeam-channel = "0.5.0" +parking_lot = "0.11.0" ansi_term = "0.12.1" glob = "0.3" -custom_error = "1.7.1" - -[badges] -travis-ci = { repository = "grego/caretaker" } -is-it-maintained-open-issues = { repository = "grego/caretaker" } -maintenance = { status = "passively-maintained" } +custom_error = "1.8.0" [profile.release] lto = true diff --git a/README.md b/README.md index 4f02e3f..30382a3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # caretaker -[![Build status](https://flat.badgen.net/travis/grego/caretaker)](https://travis-ci.org/grego/caretaker) -[![Crates.io status](https://flat.badgen.net/crates/v/caretaker)](https://crates.io/crates/caretaker) +[![Build status](https://badgen.net/travis/grego/caretaker)](https://travis-ci.org/grego/caretaker) +[![Crates.io status](https://badgen.net/crates/v/caretaker)](https://crates.io/crates/caretaker) +[![Docs](https://docs.rs/caretaker/badge.svg)](https://docs.rs/caretaker) A simple tool that loads a list of paths to watch from a TOML file. ```toml [[watch]] name = "print hello" path = "src" -command = "echo \"hello world\"" +command = "echo $EVENT_PATH" [[watch]] name = "compile sass" @@ -16,6 +17,11 @@ command = "sassc -t compressed sass/style.scss static/style.css" ``` On a change in the `path`, it executes the `command`. Directories are watched recursively. Paths can also be specified with [globs](https://docs.rs/glob/0.3.0/glob/struct.Pattern.html). +Any shell command can be used, along with pipes and so on. +By default, the shell specified in the `$SHELL` environment variable is used to parse and execute the command. +Otherwise, on Unix system, it invokes the default +Bourne shell (`sh` command), on windows [cmd.exe](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd). +Additionally, each command gets the `$EVENT_PATH` environment variable, containing the path that changed. Using [notify](https://github.com/notify-rs/notify) crate, which provides efficient event handling support for the most operating systems (apart from BSD). diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index dc283e3..0000000 --- a/src/command.rs +++ /dev/null @@ -1,173 +0,0 @@ -use serde::de::{Deserialize, Deserializer, Visitor}; -use std::borrow::Cow; -use std::fmt; -use std::ops::{Deref, DerefMut}; -use std::process; -use std::str::CharIndices; - -pub struct Command(process::Command); - -impl<'de> Deserialize<'de> for Command { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_string(CommandVisitor) - } -} - -struct CommandVisitor; - -impl<'de> Visitor<'de> for CommandVisitor { - type Value = Command; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a command string") - } - - fn visit_str(self, v: &str) -> Result { - let mut args = ArgIter::new(v).map(|cow| match cow { - Cow::Borrowed(c) => Cow::Borrowed(c.as_ref()), - Cow::Owned(c) => Cow::Owned(c.into()), - }); - let mut command = process::Command::new(args.next().unwrap_or_default()); - command.args(args); - Ok(Command(command)) - } -} - -impl Deref for Command { - type Target = process::Command; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Command { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Iterator over arguments of the command. -/// Can't simply use `split_whitespace`, since some argumenst may be quoted -/// and contain multiple words. -struct ArgIter<'a> { - source: &'a str, - chars: CharIndices<'a>, -} - -impl<'a> ArgIter<'a> { - fn new(source: &'a str) -> Self { - ArgIter { - source, - chars: source.char_indices(), - } - } -} - -impl<'a> Iterator for ArgIter<'a> { - type Item = Cow<'a, str>; - - fn next(&mut self) -> Option { - #[derive(Clone, Copy)] - enum State { - None, - Quote(char), - }; - - let mut previous; - let mut initial; - let mut owned: Option = None; - loop { - let (i, ch) = self.chars.next()?; - if !ch.is_whitespace() { - previous = ch; - initial = i; - break; - } - } - let mut last = self.source.len(); - - let mut state = match previous { - '"' | '\'' => { - initial += 1; - State::Quote(previous) - } - '\\' => { - initial += 1; - State::None - } - _ => State::None, - }; - let initial_state = state; - let mut state_changes = 0; - let mut found_whitespace = false; - - while let Some((l, c)) = self.chars.next() { - last = l; - - if (c == '"' || c == '\'') && previous != '\\' { - match state { - State::None => { - state_changes += 1; - state = State::Quote(c) - } - State::Quote(q) if c == q => { - state_changes += 1; - state = State::None - } - _ => {} - } - } else if let State::None = state { - if c.is_whitespace() { - found_whitespace = true; - break; - } - } - - if c != '\\' || previous == '\\' { - if let Some(ref mut arg) = owned { - arg.push(c); - } - } - - if c == '\\' && owned.is_none() { - owned = Some(self.source[initial..last].to_string()); - } - previous = c; - } - - if !found_whitespace && owned.is_none() { - last = self.source.len(); - }; - - match initial_state { - State::Quote(_) if state_changes == 1 => { - if let Some(ref mut arg) = owned { - arg.pop(); - } else { - last -= 1; - } - } - _ => {} - }; - - owned - .map(|s| s.into()) - .or_else(|| self.source.get(initial..last).map(|s| s.into())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_argiter() { - let source = "echo h \"hello\\\" world\" \"\" \"h\\\"\""; - let args: Vec> = ArgIter::new(source).collect(); - assert_eq!(args, &["echo", "h", "hello\" world", "", "h\""]); - } -} diff --git a/src/error.rs b/src/error.rs index 04343ec..b09f393 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,26 @@ use custom_error::custom_error; -custom_error! { pub Error - Notify{source: notify::Error} = "notify error", - Pattern{source: glob::PatternError} = "invalid glob", - IO{source: std::io::Error} = "input / output error", - Receive{source: crossbeam_channel::RecvError} = "channel receive error", +custom_error! { +/// All possible ways how caretaking may fail. +pub Error + /// Error of the underlying watch mechanism + Notify{ + /// Source of the error. + source: notify::Error + } = "Notify error: {source}", + /// The provided glob path was not valid. + Pattern{ + /// Source of the error. + source: glob::PatternError + } = "Invalid glob: {source}", + /// Input / output error + IO{ + /// Source of the error. + source: std::io::Error + } = "Input / output error: {source}", + /// Channel receive error + Receive{ + /// Source of the error. + source: crossbeam_channel::RecvError + } = "Channel receive error: {source}", } diff --git a/src/main.rs b/src/main.rs index 17c1fc3..e8b5e3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,37 @@ -pub mod command; -pub mod watch; -pub mod error; +//! A simple tool that loads a list of paths to watch from a TOML file. +//! ```toml +//! [[watch]] +//! name = "print hello" +//! path = "src" +//! command = "echo $EVENT_PATH" +//! +//! [[watch]] +//! name = "compile sass" +//! path = "sass/*.sass" +//! command = "sassc -t compressed sass/style.scss static/style.css" +//! ``` +//! On a change in the `path`, it executes the `command`. Directories are watched recursively. +//! Paths can also be specified with [globs](https://docs.rs/glob/0.3.0/glob/struct.Pattern.html). +//! Any shell command can be used, along with pipes and so on. +//! By default, the shell specified in the `$SHELL` environment variable is used to parse and execute the command. +//! Otherwise, on Unix system, it invokes the default +//! Bourne shell (`sh` command), on windows [cmd.exe](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd). +//! Additionally, each command gets the `$EVENT_PATH` environment variable, containing the path that changed. +//! +//! Using [notify](https://docs.rs/notify) crate, which provides efficient event handling +//! support for the most operating systems (apart from BSD). +#![warn(missing_docs)] +mod error; +mod watch; + +pub use error::Error; +pub use watch::{watch, Config, Watch}; + +use watch::SHELL; use ansi_term::Style; -use error::Error; +use std::env; use structopt::StructOpt; -use watch::watch; #[derive(StructOpt)] #[structopt(rename_all = "kebab-case")] @@ -13,7 +39,10 @@ use watch::watch; struct Opt { /// File to read what to watch from #[structopt(short, long, default_value = ".watch.toml")] - watch_config: String, + config: String, + /// Shell to parse and execute the commands with + #[structopt(short, long)] + shell: Option, #[structopt(subcommand)] cmd: Option, } @@ -32,7 +61,7 @@ name = \"print hello\" # Where to look for changes? path = \"src\" # What to execute on change? -command = \"echo \\\"hello world\\\"\" +command = \"echo $EVENT_PATH\" # Repeat this to watch multiple paths"; @@ -42,7 +71,7 @@ fn main() -> Result<(), Error> { let bold = Style::new().bold(); match opt.cmd { Some(Cmd::Init) => { - let config = &opt.watch_config; + let config = &opt.config; if std::fs::metadata(config).is_ok() { println!("{} already exists, exiting", bold.paint(config)) } else { @@ -51,13 +80,14 @@ fn main() -> Result<(), Error> { } } _ => { - let config = std::fs::read(&opt.watch_config)?; + let config = std::fs::read(&opt.config)?; match toml::from_slice(&config) { Ok(config) => { - watch(config)?; + let shell = opt.shell.or_else(|| env::var("SHELL").ok()); + watch(config, shell.as_deref().unwrap_or(SHELL))?; } Err(e) => { - println!("Unable to parse {}: {}", bold.paint(&opt.watch_config), e); + println!("Unable to parse {}: {}", bold.paint(&opt.config), e); } }; } diff --git a/src/watch.rs b/src/watch.rs index 36cbbe9..7926a97 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,30 +1,46 @@ -use crate::command::Command; use crate::Error; use ansi_term::Style; use crossbeam_channel::unbounded; use glob::Pattern; use notify::{immediate_watcher, RecursiveMode, Watcher}; +use parking_lot::Mutex; use serde::Deserialize; use std::convert::Infallible; use std::path::{is_separator, Path}; -use std::sync::Mutex; +use std::process::Command; +#[cfg(target_family = "unix")] +pub(crate) static SHELL: &str = "sh"; +#[cfg(target_family = "unix")] +static ARGUMENT: &str = "-c"; +#[cfg(target_family = "windows")] +pub(crate) static SHELL: &str = "cmd"; +#[cfg(target_family = "windows")] +static ARGUMENT: &str = "/c"; + +/// One path to watch #[derive(Deserialize)] -struct Watch { +pub struct Watch { + /// A name of the action to do on the path change. #[serde(default)] - name: String, - path: String, - command: Mutex, + pub name: String, + /// The path to watch for change. + pub path: String, + /// The command to execute on path change. + pub command: String, } +/// The config file of Caretaker #[derive(Deserialize)] pub struct Config { - watch: Vec, + /// A list of paths and commands to watch. + pub watch: Vec, } -pub fn watch(config: Config) -> Result { +/// Watch the paths specified in the config, executing the commands using the provided shell. +pub fn watch(config: Config, shell: &str) -> Result { use notify::event::{EventKind::*, *}; let len = config.watch.len(); @@ -33,6 +49,8 @@ pub fn watch(config: Config) -> Result { let bold = Style::new().bold(); let is_glob = |c| c == '*' || c == '?' || c == '['; + let matches = + |pattern: &Pattern, path: &Path| path.to_str().map(|s| pattern.matches(s)).unwrap_or(false); for Watch { name, @@ -61,31 +79,34 @@ pub fn watch(config: Config) -> Result { None }; + let mut cmd = Command::new(shell); + cmd.args(&[ARGUMENT, &command]); + let command = Mutex::new(cmd); + let mut watcher = immediate_watcher(move |res: Result| match res { Ok(Event { kind, paths, .. }) => match kind { Access(AccessKind::Close(AccessMode::Write)) | Create(_) | Modify(ModifyKind::Name(RenameMode::To)) | Remove(_) => { - let matches_glob = glob - .as_ref() - .map(|pattern| { - paths.iter().any(|path| { - path.to_str().map(|s| pattern.matches(s)).unwrap_or(false) - }) - }) - .unwrap_or(true); - if !matches_glob { - return; - } + for path in &paths { + if !glob + .as_ref() + .map(|pattern| matches(pattern, path)) + .unwrap_or(true) + { + return; + } - println!("{:?} changed, running {}", &paths, bold.paint(&name)); - if let Err(e) = command - .lock() - .map_err(|e| e.into()) - .and_then(|mut c| c.status().map_err(|e| e.into())) - { - tx.send(e).unwrap(); + println!("{:?} changed, running {}", path, bold.paint(&name)); + if let Err(e) = command + .lock() + .env("EVENT_PATH", path) + .status() + .map_err(|e| e.into()) + { + tx.send(e).unwrap(); + } } } _ => {}