README
¶
ifchanged
Run shell commands when files change.
Overview
ifchanged hashes files on each run and executes configured commands for any that changed since last time. It's for automation that should trigger on file change. applying SQL migrations, regenerating code, syncing assets, without setting up a full watch daemon or CI pipeline.
It is not a daemon. It does not watch in the background. Run it manually, from a Makefile, or in a pre-commit hook.
Installation
With a Go toolchain installed:
go install github.com/Lexographics/ifchanged@latest
The binary is placed in $(go env GOPATH)/bin. Ensure that directory is on your PATH.
Build from source:
git clone github.com/Lexographics/ifchanged
cd ifchanged
go build -o ifchanged .
Usage
ifchanged [flags] [dir]
Without arguments, ifchanged searches the current directory for *.ifchanged.yaml files, hashes the matched files, and runs commands for any that changed.
# Run in current directory
ifchanged
# Run against a specific directory (searches that dir for *.ifchanged.yaml files)
ifchanged ./src
# Preview what would run without executing
ifchanged --dry-run
# Clear stored hashes then immediately run commands (force re-run)
ifchanged --clean
# Clear stored hashes and exit (next run will treat all files as changed)
ifchanged clean
# Use a specific config file
ifchanged --config deploy.ifchanged.yaml
# Use a custom state file
ifchanged --state ./.ifchanged-state.yaml
Examples
See ./examples/ for runnable example directories (SQL migrations, SQL RLS policies etc.).
Config file format
Config files must match *.ifchanged.yaml. Place them in the directory you run ifchanged from.
# examples/sql-migrations/migrations.ifchanged.yaml
include:
- sql/migrations/**/*.sql
exclude:
- sql/migrations/scratch.sql
command:
- psql -U postgres -d myapp -f {{file}}
include— glob patterns for files to watch.**matches any number of path segments.exclude— glob patterns to skip. Evaluated afterinclude.command— shell commands to run for each changed matched file. Placeholders are expanded per file (see Command placeholders). Commands run sequentially; if one fails, the rest are skipped for that file and the hash is not updated.has-changes— optional. Shell commands run once after processing if any matched file changed. Only global placeholders apply.no-changes— optional. Shell commands run once if no matched file changed. Only global placeholders apply.state— optional. Override the state file path for this config.
If any per-file command fails, ifchanged does not update the stored hash for that file, so it will be retried on the next run.
Command placeholders
These strings are substituted in command strings before they are passed to the shell.
Per-file placeholders (command)
Expanded once per changed file; paths use the host’s native separators except {{relFile}} and {{relFileDir}}, which use forward slashes.
| Placeholder | Meaning |
|---|---|
{{file}} |
Absolute path to the matched file |
{{fileName}} |
Base name (e.g. user_triggers.sql) |
{{fileStem}} |
Filename without extension (e.g. user_triggers) |
{{fileExt}} |
Extension including the dot (e.g. .sql) |
{{fileDir}} |
Directory containing the file (absolute) |
{{relFile}} |
Path to the file relative to the directory you run ifchanged against |
{{relFileDir}} |
Directory part of {{relFile}} |
Global placeholders (has-changes, no-changes)
| Placeholder | Meaning |
|---|---|
{{workingDirectory}} |
The directory ifchanged uses as the run root (the dir argument, or the current working directory) |
Flags
| Flag | Default | Description |
|---|---|---|
-config |
- | Path to a specific *.ifchanged.yaml file |
-state |
~/.ifchanged.yaml |
Path to state file |
-env-file |
- | Path to a .env file to load for all commands |
-dry-run |
false | Print commands that would run; don't execute or update state |
-clean |
false | Clear stored hashes for matched files, then run commands |
Subcommands
ifchanged clean [dir]
Clears stored hashes for all matched files and exits without running any commands. Use this to force a full re-run on the next invocation.
Unlike --clean, which clears hashes and then immediately runs commands, the clean subcommand only resets state.
State file
State is stored at ~/.ifchanged.yaml by default — a YAML map of configpath:filepath → SHA256 hash. Written atomically (temp file + rename). You can commit it, ignore it, or set a per-project path with --state.
Real-world examples
Apply SQL migrations only when files change:
# examples/sql-migrations/migrations.ifchanged.yaml
include:
- db/migrations/**/*.sql
command:
- psql $DATABASE_URL -f {{file}}
ifchanged # only runs psql for new/modified migration files
Regenerate protobuf bindings:
# examples/protobuf-generation/proto.ifchanged.yaml
include:
- proto/**/*.proto
command:
- protoc --go_out=. --go-grpc_out=. {{file}}
Reset and force re-run everything:
ifchanged --clean # clear hashes, then run commands immediately
ifchanged clean # clear hashes and exit (commands run on next invocation)
Dry run to audit what would execute:
ifchanged --dry-run
Contributing
Contributions are welcome! Please open issues or pull requests for bug fixes, features, or documentation improvements.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Documentation
¶
There is no documentation for this package.