598

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.

5
  • Possible cross site duplicate of: stackoverflow.com/questions/2972765/… ( although here it is on topic =) ) Mar 11, 2014 at 17:31
  • i've referenced before a cross site duplicate and it was denied :S ;) Sep 9, 2015 at 19:51
  • 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

40 Answers 40

1
2
1

For people who find this by Googling for changes to a particular file, the answer is much simpler (inspired by Gilles's answer).

If you want to do something after a particular file has been written to, here's how:

while true; do
  inotifywait -e modify /path/to/file
  # Do something *after* a write occurs, e.g. copy the file
  /bin/cp /path/to/file /new/path
done

Save this as, for example, copy_myfile.sh and put the .sh file into the /etc/init.d/ folder to have it run on startup.

1
  • Shares the problem with Giles' answer that it runs inotifywait on every iteration, which can be unresponsive for recursively watching very large directories. See cychoi's answer for the fix to this. Sep 9, 2015 at 19:51
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.

1

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")
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="info@example.com"
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
1
  • one-liner in three line :) I love it
    – sarki_roka
    May 30, 2021 at 10:32
1
2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.