# IF Filesystem-event Then (IFFT) [![Latest Version]][crates.io] [![Build Status]][travis] [Build Status]: https://api.travis-ci.com/braincore/ifft.svg?branch=master [travis]: https://travis-ci.com/braincore/ifft [Latest Version]: https://img.shields.io/crates/v/ifft.svg [crates.io]: https://crates.io/crates/ifft IF a filesystem event (create, write, remove, chmod) occurs in a watched folder that is not filtered out by an exclusion rule THEN execute a shell command. Use this to watch for code changes to trigger: process restart; code compilation; or test run. ## Installation If you have [rust installed](https://www.rust-lang.org/tools/install) on your machine: ``` cargo install ifft ``` Otherwise, check [releases](https://github.com/braincore/ifft/releases) for downloads. ## Usage ### Hello, world. Create a config file (`ifft.toml`) in a directory (let's say `~/src/ifft-test`): ```toml [[ifft]] # Matches everything including sub-folders if = "**/*" then = "echo hello, world." ``` Run `ifft` with the directory containing your config as the argument: ``` $ ifft ~/src/ifft-test Found config: "~/src/ifft-test/ifft.toml" ``` `ifft` found your config file. In later examples, we'll see that multiple config files can be embedded throughout the filesystem tree. Now let's create a file to trigger your ifft: ``` $ touch ~/src/ifft-test/test1 ``` You'll see the following output: ``` [2019-05-12 14:55:57Z] Event: Create("~/src/ifft-test/test1") Match from config in: "~/src/ifft-test" Matched if-cond: "**/*" [2019-05-12 14:55:57Z] Execute: "echo hello, world." from "~/src/ifft-test" Exit code: 0 Stdout: hello, world. Stderr: ``` As you can see, the triggered command's match condition, exit code, stdout, and stderr are printed. That's it. `ifft` simply listens for file changes and takes action. ### Filters Use the `not` argument to specify file patterns to filter out from triggering: ```toml [[ifft]] if = "**/*.{c,h}" not = [ "*~", "*.swp", # Filter out swap files "dist/**/*", # Filter out outputs of compilation ] then = "gcc main.c -o dist/prog" ``` `not` can also be specified at the config-level which will apply to all iffts: ``` not = [ "*~", "*.swp", # Filter out swap files "dist/**/*", # Filter out outputs of compilation ] [[ifft]] if = "**/*" then = "gcc main.c -o dist/prog" ``` A roadmap feature is to offer a flag to automatically ignore patterns in `.gitignore`. ### Working Directory By default, the working directory used to execute the `then` clause is the folder of the `ifft.toml` file being triggered. To override, use the `working_dir` argument to `[[ifft]]`. ### Path Substitution The `then` clause can use the `{{}}` placeholder which will be replaced by the path of the modified file that triggered the ifft. ### On Start If you want to automatically trigger iffts on start without any file event, use the `-r` flag. The argument will trigger any iffts with matching names. For example: ``` ifft ~/src/ifft-test -r build ``` Matches: ```toml [[ifft]] name = "build" # Triggered by -r flag not = ["target/*"] then = "cargo build" ``` This is useful to ensure that projects are built on boot without having to wait for a file event. You can also use the `-q` flag to quit after the `-r` flag triggers have completed. This can be used to initiate a one-time build or clean without listening for changes afterwards. ### Distributing `iffts` Imagine you have the following filesystem tree: ``` ~/src/my-app ~/src/my-app/my-c-service ~/src/my-app/my-rust-service ``` While you could create one config file `~/src/my-app/ifft.toml` with the iffts for all projects, a better approach is to create an `ifft.toml` in each of the service directories. When invoking `ifft` it will report the configs it has found: ``` $ ifft ~/src/my-app Found config: "~/src/my-app/ifft.toml" Found config: "~/src/my-app/my-c-service/ifft.toml" Found config: "~/src/my-app/my-rust-service/ifft.toml" ``` This allows you to distribute config files across your filesystem tree, which has the advantage of keeping them small and relevant to the folder they're in. ### Dependencies If you have cross-project dependencies, you may want to trigger an ifft based on another ifft. This is possible using `listen` and `emit`. Assume the following filesystem tree: ``` ~/src/my-app ~/src/my-app/my-c-service/ifft.toml ~/src/my-app/my-rust-service/ifft.toml ``` If `my-rust-service` depends on `my-c-service`, you can write the following: ```toml [[ifft]] if = "listen:../my-c-service:built" # Listens for "built" from my-c-service then = "cargo build" ``` `my-c-service/ifft.toml` can be written as follows: ```toml [[ifft]] if = "**/*.{c,h}" then = "gcc *.c -o c-service" emit = "built" # Emits "built" to listeners ``` A similar pattern is used for "on start" iffts. Use `on_start_listen`: ```toml # # my-rust-service/ifft.toml # [[ifft]] name = "build" if = "on_start_listen:../my-c-service:built" then = "cargo build" # # my-c-service/ifft.toml # [[ifft]] name = "build" then = "gcc *.c -o c-service" emit = "built" ``` Using the on start syntax (`ifft my-app -r build -q`) will execute these iffts in the correct order: first `my-c-service`; second `my-rust-service`. ### Delegation IFFT can also launch other watch/recompilation programs. For example, it's common to have a file watcher setup with `npm`/`yarn` (e.g. `yarn watch`). To delegate watching, use a `[[delegate]]` section: ```toml [[delegate]] cmd = "yarn watch" ``` Delegates are launched after "On Start" triggers. This ordering is intentional so that "On Start" can perform setup needed by delegates (e.g. `yarn install`). An optional `restart_on` field can be set with a value in `listen:path:emit` format to trigger a restart of the delegate process. Delegates are useful for making IFFT the primary file watching tool across a large multi-project repository. ## Features * Configure with a `toml` file. * Config files can be distributed throughout a filesystem tree. * Use glob patterns for `if` and `not` conditions. * Global `not` filtering and per-trigger `not` filtering. * If multiple events trigger the same `if`, `then` is only executed if an event was triggered after the last time `then` was executed. * On start, iffts with a matching name can be triggered without any file event. * Events on paths with symlink components will also have their absolute-path equivalent tested against triggers. * Dependencies * An ifft can be triggered by listening for an emitted tag from another. * On start, iffts can be ordered via a dependency graph. * Respects ignore files (hidden, `.gitignore`, ...) for config collection and folder watching. ## Platforms Tested on Linux and OS X. Untested elsewhere. ## Usage with VirtualBox Shared Folders On the guest OS, VirtualBox Shared Folders do not generate filesystem event notifications. You'll need to use a separate filesystem event forwarder such as [notify-forwarder](https://github.com/mhallin/notify-forwarder). ## Alternatives * [bazel](https://bazel.build/) for a serious incremental build system. * [watchexec](https://github.com/watchexec/watchexec) is a more full-featured program. * [entr](http://eradman.com/entrproject/) has a clean Unixy interface. ## Todo * [ ] Respect ignore files for triggered files. * [ ] Multi-level flag to control verbosity of prints. * [ ] Group events in quick succession together and trigger only once. * [ ] Allow customization of type of FS events that trigger. * [ ] Low priority: Compute the optimal path prefix for watching. * [ ] Performance: Do not compile glob before each use. Current hack to make it easy to access the glob pattern string if an error occurs.