How to Use Entr + Cron to Auto-Run Scripts When Files Change
As developers and system administrators, we often face the challenge of automating tasks that depend on file changes. Perhaps you need to recompile a project when source files are modified, restart a service when its configuration changes, or process data as soon as new files land in a directory.
The common approaches often fall short:
- Manual execution: Tedious, error-prone, and doesn’t scale.
- Polling with
watch
orwhile true
loops: Inefficient, consumes resources even when nothing changes, introduces unnecessary delays, and is generally a hack.
We need something better. We need event-driven automation that’s both immediate and persistent. Enter entr
and cron
.
This post will show you how to combine these two powerful Unix utilities to build robust, reactive, and persistent automation workflows.
The Problem: Event-Driven Persistence
entr
(event-notify-then-run) is a fantastic utility for running arbitrary commands when files change. It’s lean, fast, and uses kernel facilities (like inotify
on Linux) for efficiency.
cron
is the classic Unix job scheduler. It’s rock-solid for running tasks at specific times or intervals, or even at system boot.
The challenge is that entr
typically runs in the foreground. If your terminal closes, or the system reboots, entr
stops. We need a way to ensure our entr
watcher is always running, or at least restarted reliably. This is where cron
comes in.
By using cron
to manage entr
as a background process, we achieve:
- Event-driven execution: Scripts run only when needed.
- Persistence: The watcher restarts automatically after reboots.
- Robustness: We can implement checks to ensure it’s always running.
Let’s break down entr
and cron
individually, then combine them.
Understanding entr
entr
is designed to execute a command when any of the files piped into its standard input change.
Installation
entr
is often available in your distribution’s package manager.
Debian/Ubuntu:
sudo apt update
sudo apt install entr
Fedora/RHEL:
sudo dnf install entr
macOS (with Homebrew):
brew install entr
Basic entr
Usage
The core syntax is [list of files] | entr [command]
.
Example 1: Watching a single file
Let’s create a simple script that entr
will execute.
echo '#!/bin/bash
echo "--- File changed at $(date) ---"
cat file.txt
' > script_on_change.sh
chmod +x script_on_change.sh
echo "Initial content" > file.txt
Now, watch file.txt
and run script_on_change.sh
whenever it changes:
ls file.txt | entr ./script_on_change.sh
# No output immediately, entr is waiting.
# Now, open another terminal and modify file.txt:
echo "New content after modification" > file.txt
# Back in the entr terminal, you'll see:
--- File changed at Fri Oct 27 10:30:00 AM UTC 2023 ---
New content after modification
Example 2: Watching multiple files and directories (recursively)
To watch files recursively in a directory, use find
. We also often want entr
to execute the command in a shell to allow for pipelines or complex commands, and perhaps clear the screen between runs.
# Create some dummy files
mkdir my_project
echo "console.log('hello');" > my_project/app.js
echo "body { color: blue; }" > my_project/style.css
# Create a build script
echo '#!/bin/bash
echo "--- Building project at $(date) ---"
echo "Simulating build process..."
sleep 0.5
echo "Build complete."
' > my_project/build.sh
chmod +x my_project/build.sh
Now, watch all .js
and .css
files in my_project
recursively, clear the screen (-c
), and execute the build script in a shell (-s
):
find my_project -name "*.js" -o -name "*.css" | entr -c -s ./my_project/build.sh
# No output immediately, entr is waiting.
# Now, modify a file:
echo "console.log('updated');" >> my_project/app.js
# Back in the entr terminal, you'll see (screen clears first):
--- Building project at Fri Oct 27 10:35:00 AM UTC 2023 ---
Simulating build process...
Build complete.
Useful entr
Flags
-c
: Clear the screen before executing the command.-d
: Watch new files created in watched directories. Essential for projects where new files are frequently added.-p
: Pause for a short period (debounce) before executing the command. Useful to prevent multiple rapid executions during a batch of saves.-s
: Execute the command usingsh -c
. This is crucial if your command involves pipes, redirections, or other shell features. Without-s
,entr
executes the command directly.-z
: Do not exit if the command exits with a non-zero status. Useful if your script might fail but you wantentr
to keep watching.-r
: Reloadentr
itself if its input files change. Useful if yourfind
command or the list of filesentr
is watching needs to be re-evaluated.-L <pidfile>
: Writeentr
’s PID to a file. Useful for managingentr
processes directly, though we’ll use a wrapper script for ourcron
setup.-0
: Read NUL-terminated input. Use this withfind ... -print0
for robust handling of filenames with spaces or special characters.
For our persistent setup, -s
and potentially -d
and -p
will be common.
Understanding cron
cron
allows you to schedule commands or scripts to run automatically at specified intervals or times. Each user has their own crontab, and there’s also a system-wide crontab.
Basic cron
Usage
To edit your user’s crontab:
crontab -e
This will open your crontab in your default editor. Each line represents a job.
Cron Job Syntax
A cron job line has six fields:
minute hour day_of_month month day_of_week command_to_execute
- minute: (0-59)
- hour: (0-23)
- day_of_month: (1-31)
- month: (1-12 or Jan-Dec)
- day_of_week: (0-7, where 0 and 7 are Sunday)
- command_to_execute: The command or script to run.
Special strings simplify common schedules:
@reboot
: Run once at system startup.@yearly
or@annually
: Run once a year (0 0 1 1 *).@monthly
: Run once a month (0 0 1 * *).@weekly
: Run once a week (0 0 * * 0).@daily
or@midnight
: Run once a day (0 0 * * *).@hourly
: Run once an hour (0 * * * *).
Important Cron Considerations:
- Environment: The cron environment is very minimal.
PATH
is often restricted. Always use full absolute paths to executables (e.g.,/usr/bin/entr
,/bin/bash
,/usr/bin/find
) and scripts, or explicitly setPATH
at the top of your crontab. - Output: By default, cron emails
stdout
andstderr
to the user. For long-running or frequent jobs, redirect output to log files (e.g.,command >> /path/to/log.log 2>&1
). - User: Jobs in your user’s crontab run as your user. Jobs in
/etc/crontab
or/etc/cron.d/
can specify the user.@reboot
inroot
’s crontab is common for system services.
The Synergy: entr
+ cron
for Persistent File Watching
The goal is to have entr
run reliably in the background, even after reboots. We’ll use cron
’s @reboot
directive combined with a wrapper script to achieve this.
Strategy: @reboot
with a Watchdog Script
Instead of directly running entr
in crontab
, we’ll use a shell script that checks if entr
is already running (to prevent multiple instances) and, if not, starts it in the background, redirecting output to a log file.
Scenario: Automatically restart a hypothetical web_server.py
when any .py
or .conf
file changes in /var/www/my_app/config
or /var/www/my_app/src
.
-
Create the script
entr
will execute (restart_web_server.sh
): This script will contain the logic to be run when files change.#!/bin/bash LOG_FILE="/var/log/my_app_watcher.log" echo "[$(date)] Detected file change. Restarting web server..." >> $LOG_FILE # Simulate stopping and starting a web server # Replace this with your actual service restart command # e.g., systemctl restart my_web_server.service echo "Stopping existing server..." >> $LOG_FILE pkill -f "python my_web_server.py" # Kills any running instances of our dummy server sleep 1 echo "Starting new server..." >> $LOG_FILE # Start the server in the background, redirecting its output nohup python3 /var/www/my_app/my_web_server.py > /var/log/my_app_server.log 2>&1 & SERVER_PID=$! echo "Server started with PID: $SERVER_PID" >> $LOG_FILE echo "--- Web server restart complete ---" >> $LOG_FILE
Make it executable:
chmod +x /usr/local/bin/restart_web_server.sh
Note: For system-level services, you’d typically use
systemctl restart my_service.service
or similar instead of directpkill
andnohup
. The example usesnohup
for simplicity in a self-contained demonstration. -
Create a dummy web server script (
my_web_server.py
):# /var/www/my_app/my_web_server.py import time import datetime import os log_path = "/var/log/my_app_server.log" def log(message): timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(log_path, "a") as f: f.write(f"[{timestamp}] [PID {os.getpid()}] {message}\n") log("Web server started.") print(f"Web server running with PID {os.getpid()}") # This goes to stdout/stderr of nohup while True: log("Server active...") time.sleep(10)
-
Create the
entr
watchdog script (start_entr_watcher.sh
): This script will be called bycron
. It checks for an existingentr
process and starts one if it’s not running.#!/bin/bash # --- Configuration --- WATCH_DIR="/var/www/my_app" SCRIPT_TO_RUN_ON_CHANGE="/usr/local/bin/restart_web_server.sh" LOG_FILE="/var/log/my_app_watcher.log" PID_FILE="/var/run/my_app_entr_watcher.pid" # Use /var/run for system-wide PIDs # Ensure log directory exists mkdir -p $(dirname $LOG_FILE) touch $LOG_FILE chmod 644 $LOG_FILE # Ensure permissions are appropriate for the user cron runs as echo "--- Starting entr watchdog at $(date) ---" >> $LOG_FILE # --- Check for existing entr process --- if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if kill -0 "$PID" 2>/dev/null; then echo "[$(date)] entr watcher already running with PID $PID. Exiting." >> $LOG_FILE exit 0 else echo "[$(date)] Stale PID file found. Removing $PID_FILE." >> $LOG_FILE rm -f "$PID_FILE" fi fi # --- Start entr watcher --- echo "[$(date)] Starting entr file watcher..." >> $LOG_FILE # Build the entr command. Using -0 with find -print0 is safest for filenames. # -p: Debounce changes, -s: Run command in a shell # nohup ... & makes it run in the background, detached from the shell nohup /usr/bin/find "$WATCH_DIR" -type f \( -name "*.py" -o -name "*.conf" \) -print0 \ | /usr/bin/entr -0 -p -s "$SCRIPT_TO_RUN_ON_CHANGE" \ >> "$LOG_FILE" 2>&1 & # Capture the PID of the entr process ENTR_PID=$! echo "$ENTR_PID" > "$PID_FILE" echo "[$(date)] entr watcher started with PID $ENTR_PID." >> "$LOG_FILE" echo "--- entr watchdog completed at $(date) ---" >> $LOG_FILE
Make it executable:
chmod +x /usr/local/bin/start_entr_watcher.sh
Permissions Note: If you put
PID_FILE
in/var/run
, the cron job needs write permissions there. If running as a non-root user, you might need to use a directory like/tmp
or~/.local/run/
for the PID file and logs, or ensure/var/run/my_app
exists and is writable by your user. For system-level services,root
’s crontab is often used, and it has access to/var/run
. -
Add the cron job (
crontab -e
): We want this script to run once when the system boots up. If you’re managing a system service, you’d likely add this toroot
’s crontab (sudo crontab -e
).# Open your crontab (or root's crontab if managing system services) crontab -e
Add the following line to your crontab:
# Run the entr watchdog script at system reboot @reboot /usr/local/bin/start_entr_watcher.sh
Testing and Verification
-
Manually test the
restart_web_server.sh
script:/usr/local/bin/restart_web_server.sh
Check
/var/log/my_app_watcher.log
and/var/log/my_app_server.log
for output. Verify the Python server is running:ps aux | grep "python3 /var/www/my_app/my_web_server.py"
user 12345 0.1 0.1 12345 6789 ? Sl 10:00 0:00 python3 /var/www/my_app/my_web_server.py
-
Manually test the
start_entr_watcher.sh
script: This simulates what@reboot
will do./usr/local/bin/start_entr_watcher.sh
Check
/var/log/my_app_watcher.log
. It should indicateentr
starting. Check ifentr
is running:ps aux | grep entr
user 12346 0.0 0.0 12345 6789 ? Ss 10:01 0:00 /usr/bin/entr -0 -p -s /usr/local/bin/restart_web_server.sh user 12347 0.0 0.0 12345 6789 ? Sl 10:01 0:00 /usr/bin/find /var/www/my_app -type f ( -name *.py -o -name *.conf ) -print0
(You might see the
find
process running as a child ofentr
.)Try running
start_entr_watcher.sh
again immediately. It should log thatentr
is already running and exit. -
Test the full flow (modify a file): Make a change to one of the watched files (e.g.,
/var/www/my_app/my_web_server.py
):echo "# New line" >> /var/www/my_app/my_web_server.py
Check
/var/log/my_app_watcher.log
and/var/log/my_app_server.log
. You should see messages indicating the web server was restarted byentr
.# Excerpt from /var/log/my_app_watcher.log after file change: [2023-10-27 10:45:01] Detected file change. Restarting web server... Stopping existing server... Starting new server... Server started with PID: 12348 --- Web server restart complete --- # Excerpt from /var/log/my_app_server.log: [2023-10-27 10:45:00] [PID 12345] Server active... [2023-10-27 10:45:01] [PID 12345] Web server started. # Old server log ends here [2023-10-27 10:45:01] [PID 12348] Web server started. # New server log starts here [2023-10-27 10:45:11] [PID 12348] Server active...
-
Reboot and verify: Reboot your system. After it comes back up, check
ps aux | grep entr
and your log files.entr
should be running and watching.
Important Considerations and Best Practices
- Absolute Paths: Always, always, always use full absolute paths in cron jobs and scripts called by cron. The
PATH
environment variable in cron is often very limited. - Logging: Redirect all
stdout
andstderr
to log files. This is your primary debugging tool when things go wrong. Without it, you’ll be flying blind. - Permissions: Ensure the user running the cron job (and thus the
entr
watcher and its triggered script) has the necessary read/write permissions for watched directories, log files, PID files, and any other resources it interacts with. - PID Files: Crucial for managing long-running background processes. They prevent multiple instances from starting and help you stop or restart processes.
- Error Handling: Your
restart_web_server.sh
script should ideally include robust error handling. What ifpkill
fails? What if thepython
command fails? Log errors and potentially implement retry logic or notifications. - Resource Usage: While
entr
is efficient, watching an extremely large number of files or highly active directories can still consume resources. Monitor CPU and memory if you’re watching vast file trees. - Systemd: For truly robust system services on Linux,
systemd
service units are generally preferred over@reboot
cron jobs.systemd
offers better dependency management, logging, automatic restarts, and process control. However, theentr
+cron
pattern is a fantastic, simpler alternative whensystemd
setup is overkill or not immediately feasible, especially for user-specific development tasks. - Alternative File Watchers: For specific needs,
inotifywait
(part ofinotify-tools
on Linux) orfswatch
are alternatives.inotifywait
is lower-level and powerful but requires more scripting.fswatch
is cross-platform.entr
strikes a great balance of power and simplicity.
Conclusion
Combining entr
and cron
unlocks a powerful pattern for persistent, event-driven automation. Whether you’re a developer tired of manually rebuilding projects, a system admin needing to react to config changes, or just someone looking to make their system more reactive, this approach offers a robust and elegant solution.
By leveraging entr
’s efficient file watching and cron
’s reliable scheduling, you can build self-managing workflows that respond immediately to changes, without the overhead of constant polling. Give it a try, and streamline your automated tasks!