10 breaking releases

0.11.0 Oct 17, 2021
0.10.0 Apr 26, 2021
0.9.0 Apr 26, 2021
0.8.0 Feb 2, 2021
0.2.0 Mar 5, 2019

#934 in Filesystem

MIT license

57KB
1K SLoC

IF Filesystem-event Then (IFFT) Latest Version Build Status

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 on your machine:

cargo install ifft

Otherwise, check releases for downloads.

Usage

Hello, world.

Create a config file (ifft.toml) in a directory (let's say ~/src/ifft-test):

[[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:

[[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:

[[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:

[[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:

[[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:

#
# 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:

[[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.

Alternatives

  • bazel for a serious incremental build system.
  • watchexec is a more full-featured program.
  • entr 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.

Dependencies

~9–19MB
~256K SLoC