ifft 0.11.0

IF Filsystem-event Then...
# 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.