642

I want a quick and simple way to execute a command whenever a file changes. I want something very simple, something I will leave running on a terminal and close it whenever I'm finished working with that file.

Currently, I'm using this:

while read; do ./myfile.py ; done

And then I need to go to that terminal and press Enter, whenever I save that file on my editor. What I want is something like this:

while sleep_until_file_has_changed myfile.py ; do ./myfile.py ; done

Or any other solution as easy as that.

BTW: I'm using Vim, and I know I can add an autocommand to run something on BufWrite, but this is not the kind of solution I want now.

Update: I want something simple, discardable if possible. What's more, I want something to run in a terminal because I want to see the program output (I want to see error messages).

About the answers: Thanks for all your answers! All of them are very good, and each one takes a very different approach from the others. Since I need to accept only one, I'm accepting the one that I've actually used (it was simple, quick and easy-to-remember), even though I know it is not the most elegant.

4
  • Possible cross site duplicate of: stackoverflow.com/questions/2972765/… ( although here it is on topic =) ) Mar 11, 2014 at 17:31
  • 4
    The solution by Jonathan Hartley builds on other solutions here and fixes big problems that the top-voted answers have: missing some modifications and being inefficient. Please change the accepted answer to his, which also is being maintained on github at github.com/tartley/rerun2 (or to some other solution without those flaws)
    – nealmcb
    Nov 23, 2015 at 16:12
  • watch -g terminates when output of the command changes, e.g.: while :; do watch -gn .1 stat -c %Z file.tex && pdflatex -interaction batchmode file.tex && pkill -HUP mupdf; done. Where : evaluates to true and -n .1 runs the command (stat) every 0.1 seconds. stat -c %Z gives time file was last accessed. Downside is this can only be stopped with ^Z, kill %1
    – Zaz
    Sep 24, 2021 at 20:39
  • 1
    @Zaz Instead of while :; do …; done, I usually use while sleep 1; do …; done. This way, I can easily ^C during the sleep call. Plus, it avoids hogging the CPU by limiting the loop to run at most at 1Hz (adjustable by changing the sleep parameters). Sep 27, 2021 at 20:17

41 Answers 41

1
2
2

I wrote a Python program to do exactly this, called rerun.

UPDATE: This answer is a Python script that polls for changes, which is useful in some circumstances. For a Linux-only Bash script that uses inotify, see my other answer, search this page for 'rerun2'.

Install for Python2 or Python3 with:

pip install --user rerun

and usage is very simple:

rerun "COMMAND"

The command is expected as a single arg, not a sequence of space-separated args. Hence quote it as shown, which reduces any extra escaping you'd have to add. Just type the command as you would have typed it at the command line, but surrounded by quotes.

By default it watches all files in or under the current directory, skipping things like known source control dirs, .git, .svn, etc.

Optional flags include '-i NAME' which ignores changes to named files or directories. This can be given multiple times.

Since it's a Python script, it needs to run the command as a sub-process, and we use a new instance of the user's current shell to interpret 'COMMAND' and decide what process to actually run. However, if your command contains shell aliases and the like which are defined in .bashrc, these will not be loaded by the subshell. To fix this, you can give rerun a '-I' flag, to use interactive (aka 'login') subshells. This is slower and more error-prone than starting a regular shell, because it has to source your .bashrc.

I use it with Python 3, but last I checked rerun still worked with Python 2.

Double-edged sword is that it uses polling instead of inotify. On the upside, this means it works on every OS. Plus, it's better than some other solutions shown here in terms of only running the given command once for a bunch of filesystem changes, not once per modified file, while at the same time it does run the command a second time if any files change again while command is running.

On the downside, polling means that there is a 0.0 to 1.0 second latency, and of course it's slow to monitor extremely large directories. Having said that, I've never encountered a project large enough that this is even noticeable so long as you use '-i' to ignore big things like your virtualenv and node_modules.

Hmmm. rerun has been indispensible to me for years - I basically use it eight hours every day for running tests, rebuilding dot files as I edit them, etc. But now I come to type this up here, it's clear that I need to switch to a solution that uses inotify (I no longer use Windows or OSX.) and is written in Bash (so it works with aliases without any extra fiddling.)

2
  • This answer is not remotely as good as my other answer (rerun2) Sep 9, 2015 at 21:50
  • 1
    From all solutions I have tried for Mac OS X in 2020 rerun gave the best results for running make test on any projects (eg. Elixir, Python, ...) $ pip3 install rerun $ rerun --verbose --ignore=_build --ignore=deps "make test-unit" Feb 25, 2020 at 20:31
2

Facebook's Watchman has built some community. You might already be using a tool that requires it.

It detects new files matching given patterns. For large projects, unlike entr, I don't need to bump system file limits and restart the computer. The following glob just works, against all project e.g. Python files.

watchman-make --pattern '**/*.py' --run './myfile.py'

It waits for a file to change before it rebuilds. You may want to run the command once without changes, then start watching.

(export CMD="$SHELL -c './myfile.py'" && eval "$CMD"; watchman-make --pattern '**/*.py' --run "$CMD")
1

Basic usage

Here is a solution that does not require installing more software and works out of the box.

tail -q --follow=name myfile.txt | head -n 0

This command exits under the following conditions:

  • A line is added to myfile.txt after the command is run
  • The myfile.txt is replaced with another after the command is run

You say you are using vim, and vim will replace the file on save. I have tested this works with vim.

You can ignore the output of this command, it may mention something like:

tail: ‘myfile.txt’ has been replaced; following end of new file

Advanced usage

You can combine this with timeout to return true or false. You can use it like this:

timeout 5s bash -c 'tail -q --follow=name pipe 2> /dev/null | head -n 0' && echo changed || echo timeout

Discussion

tail uses inotify under the hood. That's how you get this fancy asynchronous behavior without any polling. There is probably some other standard unix program that uses inotify which we can abuse more elegantly.

Sometimes these commands will exit right away but if you immediately run them a second time then they work as advertised. I have made an off-by-one error somewhere, please help me correct this.

On RHEL I can use:

timeout 5s sh -c 'gio monitor pipe | head -n 0' && echo changed || echo timeout

But I am not sure if that is portable.

1

If you don't want to install anything new for this, here is a small shell-script you can put in your path (e.g. under $HOME/bin). It runs a command when the provided one or more files are changed. For example:

$ onchange './build' *.txt
#!/bin/sh
cmd="$1"; shift
files="$@"
changed() { tar -c $files | md5sum; } # for on every save use: `stat -c %Z $files`
while true; do
  if [ "$(changed)" != "$last" ]; then
    last="$(changed)"
    $cmd
  fi
  sleep 1
done

It tars, and then hashes the contents of the files and/or directories, so it won't run every time you compulsively hit CTRL-S (or type :w), but only once something actually changes. Note it will check every second, so don't include to much or your machine could get slow. If you want it to run on every save, use stat in stead (see comment). Also, for mac md5sum is called md5 if I remember correctly.

Neat little trick: The moment you want to use it, you'll probably want to repeat the last command you just ran, but over and over. You can use the !! shortcut to 'inject' the last command into this one:

$ onchange "!!" *.txt
1

for those that are using docker, none of these solutions would work because files modified on host side do not trigger file changes inside docker containers. my solution was a simple script:

#!/bin/bash

command="ls -al *"
out=$($command | md5sum)
echo $out
exit
last=$($command | md5sum)
while true
do
    if [ "$out" != "$last" ]
    then
        #command to run when change is detected

        touch src/index.js
        out=$($command | md5sum)
        echo "change detected"
    fi
    sleep 5
    last=$($command | md5sum)
done

I made this script to be able to develop for nodejs/react using nodemon to restart nodejs server as needed.

0

The 'fido' tool may be yet another option for this need. See https://www.joedog.org/fido-home/

1
  • Please read How do I recommend software for some tips as to how you should go about recommending software. You should provide at least a link, some additional information about the software itself, and how it can be used to solve the problem in the question.
    – DavidPostill
    Mar 20, 2017 at 14:24
0

Description

This will watch a file for changes and execute whatever command (including further arguments) was given as second statement. It will also clear the screen and print the time of last execution. Note: you can make the function more (or less) reactive by changing the number of seconds the function should sleep after each while loop cycle.

Example usage

watch_file my_file.php php my_file.php

This line will watch a php file my_file.php and run through php interpreter whenever it changes.


Function definition

function watch_file (){

### Set initial time of file
LTIME=`stat -c %Z $1`
printf "\033c"
echo -e "watching: $1 ---- $(date '+%Y-%m-%d %H:%M:%S')\n-------------------------------------------\n"
${@:2}

while true
do
   ATIME=`stat -c %Z $1`

   if [[ "$ATIME" != "$LTIME" ]]
   then
    printf "\033c"
    echo -e "watching: $1 ---- $(date '+%Y-%m-%d %H:%M:%S')\n-------------------------------------------\n"
    ${@:2}
    LTIME=$ATIME
   fi
   sleep 1
done
}

Credit

This is basically a more general version of VDR's answer.

0

find can do the trick.

while true; do
    find /path/to/watched/file -ctime 1s | xargs do-what-you-will
done

find -ctime 1s prints the filename if it was changed in the last 1s.

1
  • Nice naïve idea, but will keep the CPU busy all the time, will not catch changes if the command takes longer than 1s, will run the same command multiple times if it takes less than 1s, and will fail if the filename has spaces or special characters (try using -exec instead of xargs). Jan 29, 2019 at 20:40
0

I had a slightly different situation. But I feel this may be useful to someone reading this question.

I needed to be notified when a log file changed size, but not necessary immediately. And it could be days or weeks in the future, so I could not use inotify (which was not installed/activated on that server anyway) on the command line (I didn't want to use nohup or similar). So I decided to run a bash script on cron to check

The script writes the file size of the watched file in a text file and on every cron run and checks, if that value has changed and mail the last line to me if changed

#!/bin/bash
FILE_TO_WATCH="/path/to/log_file.log"
FILESIZE_FILE="/path_to/record.txt"
SUBJECT="Log file 'log_file.log' has changed"
MAILTO="[email protected]"
BODY="Last line of log file:\n"
LAST_LINES=1

# get old recorded file size from file
OLD_FILESIZE=$(cat "${FILESIZE_FILE}")
# write current file size into file
stat --printf="%s" "${FILE_TO_WATCH}" > "${FILESIZE_FILE}"
# get new recorded file size from file
NEW_FILESIZE=$(cat "${FILESIZE_FILE}")


if [ "${OLD_FILESIZE}" != "${NEW_FILESIZE}" ]; then
    echo -e "${BODY}"$(tail -${LAST_LINES} ${FILE_TO_WATCH}) | mail -s "${SUBJECT}" "${MAILTO}"
fi
0

inotifywait one-liner

This uses inotifywait while avoiding the use of while read -r:

inotifywait -q --format '%f' -e close_write,moved_to -m . |
    grep --line-buffered -F -x 'myfile.py' |
    xargs -l -i './myfile.py'

Explanation: inotifywait outputs a line with the filename when it detected a change, grep filters the targeted filename, and xargs executes a command for each filename.

inotifywait parameters:

  • -q: Remove stderr messages.
  • --format '%f' Only output the filename, we don't filter on events anyway.
  • -e close_write,moved_to Detect only close_write (file has been written to), and moved_to (most editors use swap files, and move the buffer to the file when saving).
  • -m Keep listening indefinitely (press CTRL+C to interrupt).
  • . Target directory that contains the targeted file.

grep parameters:

  • --line-buffered: Flush lines immediately, treat as stream (like sed).
  • -F: Literal filename, don't parse regular expression (otherwise we'd need to escape the dot: myfile\.py).
  • -x: Match the whole line, not just a substring of the filename.

xargs parameters:

  • -l: Execute for each input line, don't gather lines up.
  • -i: Prevent the filename being added as an argument to the command, it replaces {} in the command with the input line.

Generic function to execute command on file change

For a more generic case, you may use this function:

exec-onchange()
{
    local file="$1"
    shift
    
    # strip path
    local filename="${file##*/}"
    
    # strip filename
    local path="${file%/*}"
    if [ -z "$path" ]; then path="."; fi
    
    # catch a custom command
    local cmd="$@"
    local literalFlag=""
    if [ -z "$cmd" ]; then cmd="$path/$filename"; literalFlag="-Fx"; fi
    
    exec inotifywait -q --format '%f' -e close_write,moved_to -m "$path" |
    grep --line-buffered $literalFlag "$filename" |
    xargs -l -i /bin/bash -c "$cmd"
}

Usage:

exec-onchange [file-to-watch] [command-to-execute]

Example usage:

exec-onchange myfile.py

Note that the argument is literal, unless a custom command is added, in which case the first argument becomes a regular expression. Although in this case, regular expressions are only allowed for matching the filename, and not the path.

This function allows for more complex usage. Such as automatically compiling, while running the compiled executable as soon as compilation is complete (by using a separate exec-onchange, compilation can be done while still running build/main):

exec-onchange 'src/.*\.cpp' 'echo "{} changed"; gcc src/*.cpp -o build/main' &
exec-onchange build/main
0
0

I used incron. This needs to be installed but it's easy to use and set up.

incron is an "inotify cron" system. It consists of a daemon and a table manipulator. You can use it a similar way as the regular cron. The difference is that the inotify cron handles filesystem events rather than time periods.

1
2

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .