71

I want to run a shell script when a specific file or directory changes.

How can I easily do that?

kenorb
  • 118,428
  • 63
  • 588
  • 624
Drew LeSueur
  • 16,653
  • 27
  • 85
  • 96
  • 1
    I have a post I think is basically the same : http://stackoverflow.com/q/2972765/119790 – Ian Vaughan Feb 10 '11 at 17:19
  • 1
    @MerlynMorgan-Graham I'd move this to superuser, because the answer might not have anything do with programming - i.e. there might be some program or configuration option that can be used, without any programming needed. I had the same question, and searched superuser first :p – Benubird May 31 '13 at 09:18
  • dupe https://superuser.com/questions/181517/how-to-execute-a-command-whenever-a-file-changes – giorgio79 Sep 04 '18 at 12:10

11 Answers11

45

You may try entr tool to run arbitrary commands when files change. Example for files:

$ ls -d * | entr sh -c 'make && make test'

or:

$ ls *.css *.html | entr reload-browser Firefox

or print Changed! when file file.txt is saved:

$ echo file.txt | entr echo Changed!

For directories use -d, but you've to use it in the loop, e.g.:

while true; do find path/ | entr -d echo Changed; done

or:

while true; do ls path/* | entr -pd echo Changed; done
kenorb
  • 118,428
  • 63
  • 588
  • 624
  • 3
    `entr` is the simplest, most composable and unix-y tool for the job. Love it. `incron` can be replaced with `entr` and a process manager like `runit`, `s6` or even `systemd`. – clacke Jul 20 '16 at 12:29
  • 3
    This. it's 10 times more simpler than inotifywait – August Feb 25 '18 at 23:54
  • I have a script regularly appending to a log file. When I use `entr` to monitor that log file and `touch` the log, everything works fine, but when the script appends to the file, `entr` fails. This may be because I have noatime set in my fstab for my ssd - but that only stops the updating of the access time not the modify time, so this confuses me. I have then tried `entr -cdr` on the directory of files that are updated with the log. That recognizes with the directory contents change, but the `-r` does not work. The `entr` process just ends. – Diagon Jun 05 '19 at 04:42
  • 1
    I turned this into [a question](https://stackoverflow.com/questions/56454520/using-entr-how-is-update-identified-noatime-troubles-and-why-does-r-not-w) – Diagon Jun 05 '19 at 05:09
  • Thank you for this. Excellent. Much better than inotify or when-changed. – Tails Nov 23 '19 at 10:24
33

I use this script to run a build script on changes in a directory tree:

#!/bin/bash -eu
DIRECTORY_TO_OBSERVE="js"      # might want to change this
function block_for_change {
  inotifywait --recursive \
    --event modify,move,create,delete \
    $DIRECTORY_TO_OBSERVE
}
BUILD_SCRIPT=build.sh          # might want to change this too
function build {
  bash $BUILD_SCRIPT
}
build
while block_for_change; do
  build
done

Uses inotify-tools. Check inotifywait man page for how to customize what triggers the build.

Bruno Bronosky
  • 54,357
  • 9
  • 132
  • 120
Dominykas Mostauskis
  • 6,521
  • 2
  • 42
  • 58
21

Use inotify-tools.

The linked Github page has a number of examples; here is one of them.

#!/bin/sh

cwd=$(pwd)

inotifywait -mr \
  --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' \
  -e close_write /tmp/test |
while read -r date time dir file; do
       changed_abs=${dir}${file}
       changed_rel=${changed_abs#"$cwd"/}

       rsync --progress --relative -vrae 'ssh -p 22' "$changed_rel" \
           usernam@example.com:/backup/root/dir && \
       echo "At ${time} on ${date}, file $changed_abs was backed up via rsync" >&2
done
tripleee
  • 139,311
  • 24
  • 207
  • 268
Frédéric Hamidi
  • 240,249
  • 39
  • 455
  • 462
  • 13
    [incron](http://inotify.aiken.cz/?section=incron&page=about&lang=en) is another option. – Dennis Williamson Oct 30 '10 at 21:23
  • 2
    fanotify is another option. Builded on top of inotify. It has some improvements to inotify, for example it can notify file changes within a specific directory. – Raydel Miranda Dec 04 '13 at 19:27
  • 38
    It would be nice if this answer included the relevant information instead of being just a link. – Darkhogg Dec 03 '15 at 11:32
  • @Darkhogg, it would be nice indeed. I had only been a member for a few weeks when I answered this question, and I did not know the site rules very well back then. In retrospect, I should have voted to close this question instead of providing a poor, link-only answer. If it ever gets unaccepted, I will delete it right away. – Frédéric Hamidi Dec 04 '15 at 11:24
  • 4
    I don't think simply posting a link to a man page is very helpful; it'd be much better if you gave the command to install the package, and examples of common usage, and _then_ linked to the full docs if the visitor still needs more information. – Ian Dunn Nov 18 '16 at 23:05
  • @Ian, yup, this is probably my worst answer on this site. It is very old, and it is accepted so I cannot delete it. I choose to keep it unchanged as a kind of testimony about those times. I use comments now to achieve the same thing, of course, although not everyone agrees with that either. – Frédéric Hamidi Nov 18 '16 at 23:27
  • Just a small addition, since I didn't see that here anywhere: incron is not able to monitor the subdirectories of that directory recursively. So unless you're monitoring a trivial directory, you'll find that incron is too limited. You might want to try [this alternative solution](https://www.splitbrain.org/blog/2011-01/07-watcher_a_recursive_incron_alternative) – Nietvoordekat Feb 09 '17 at 06:11
8

How about this script? Uses the 'stat' command to get the access time of a file and runs a command whenever there is a change in the access time (whenever file is accessed).

#!/bin/bash

while true

do

   ATIME=`stat -c %Z /path/to/the/file.txt`

   if [[ "$ATIME" != "$LTIME" ]]

   then

       echo "RUN COMMNAD"
       LTIME=$ATIME
   fi
   sleep 5
done
VDR
  • 2,223
  • 1
  • 14
  • 12
  • 7
    It's better to use `inotifywait` (inotify-tools) since it wakes up almost instantaneously when the file is updated. (Your script would wait up to 5 seconds before noticing.) Your script also has to wake up every 5 seconds, spawn a process, check the result and then go back to sleep, while this good enough for a "hack it together in 5 minutes"-script, it wastes CPU resources and should be avoided in production code. – ntninja Oct 06 '13 at 15:48
  • Thanks you for this, I usually use `enter` but sometimes I need to do something on a system where I cannot install any tools, so a method that works on any system (well, I haven’t run into anything without `stat` is super helpful. I’m using this approach without the loop to compare two times (folder time stamp and file) to see if I need to regenerate the file, and I run the check from cron. – lbutlr Mar 17 '20 at 16:38
  • @VDR when the script will work....should I have to run the example script manually to check any changes in a file e.g. example.txt occurs. As I have understood, if any change occurs it will run a command/run another script...kindly clearfy – TAMIM HAIDER Mar 24 '21 at 10:13
  • i agree with @Ibutlr sometimes we hae some machine we can not install any tool there – TAMIM HAIDER Mar 24 '21 at 10:14
  • @TAMIMHAIDER Once you invoke the script it runs in a loop comparing the given file's access time and if it is accessed it runs `echo RUN COMMAND`. You can replace the 'echo' command with other script/command that you want to run whenever a file changes. – VDR Mar 25 '21 at 16:31
  • @VDR thanks for your ans...could you please help with this scenario > in the same directory I have a file e.g. example.war and a script e.g. testscript.sh. i want if any changes occure in example.war (update/replace) will trigger testscript.sh automatically. In your example what I have understood after every change I need to run the helper script to check example.war and then it will trigger testscript.sh. – TAMIM HAIDER Mar 29 '21 at 09:57
  • @TAMIMHAIDER, just replace '/path/to/the/file.txt' to '/directory/where/example.war' and replace `echo "RUN COMMAND` with `testscript.sh` – VDR Apr 01 '21 at 15:40
3

Check out the kernel filesystem monitor daemon

http://freshmeat.net/projects/kfsmd/

Here's a how-to:

http://www.linux.com/archive/feature/124903

Brian Clements
  • 3,531
  • 21
  • 25
2

Add the following to ~/.bashrc:

function react() {
    if [ -z "$1" -o -z "$2" ]; then
        echo "Usage: react <[./]file-to-watch> <[./]action> <to> <take>"
    elif ! [ -r "$1" ]; then
        echo "Can't react to $1, permission denied"
    else
        TARGET="$1"; shift
        ACTION="$@"
        while sleep 1; do
            ATIME=$(stat -c %Z "$TARGET")
            if [[ "$ATIME" != "${LTIME:-}" ]]; then
                LTIME=$ATIME
                $ACTION
            fi
        done
    fi
}
Dan Garthwaite
  • 3,052
  • 2
  • 19
  • 31
2

As mentioned, inotify-tools is probably the best idea. However, if you're programming for fun, you can try and earn hacker XPs by judicious application of tail -f .

Yoric
  • 3,265
  • 3
  • 18
  • 26
  • Interesting, but how do you do this? tail -f is a blocking instruction, we need it to return in order to launch a script? – pdem Apr 14 '16 at 13:10
  • @pdem read it's output - any output would mean the file's changed. I'm not sure what it does if the file *shrinks* though – Xen2050 May 04 '17 at 02:28
1

Just for debugging purposes, when I write a shell script and want it to run on save, I use this:

#!/bin/bash
file="$1" # Name of file
command="${*:2}" # Command to run on change (takes rest of line)
t1="$(ls --full-time $file | awk '{ print $7 }')" # Get latest save time
while true
do
  t2="$(ls --full-time $file | awk '{ print $7 }')" # Compare to new save time
  if [ "$t1" != "$t2" ];then t1="$t2"; $command; fi # If different, run command
  sleep 0.5
done

Run it as

run_on_save.sh myfile.sh ./myfile.sh arg1 arg2 arg3

Edit: Above tested on Ubuntu 12.04, for Mac OS, change the ls lines to:

"$(ls -lT $file | awk '{ print $8 }')"
jonatan
  • 6,863
  • 2
  • 21
  • 33
1

Here's another option: http://fileschanged.sourceforge.net/

See especially "example 4", which "monitors a directory and archives any new or changed files".

slowdog
  • 5,738
  • 2
  • 24
  • 30
1

inotifywait can satisfy you.

Here is a common sample for it:

inotifywait -m /path -e create -e moved_to -e close_write | # -m is --monitor, -e is --event
    while read path action file; do
        if [[ "$file" =~ .*rst$ ]]; then             # if suffix is '.rst'
            echo ${path}${file} ': '${action}        # execute your command
            echo 'make html'
            make html
        fi
    done
pwxcoo
  • 1,951
  • 1
  • 9
  • 20
  • But how do you terminate it ? – Kalib Zen Oct 15 '20 at 15:23
  • @KalibZen You don't; in this mode, it runs forever. You can terminate the job with ctrl-C or similar, of course. – tripleee Dec 01 '20 at 19:14
  • @tripleee it's ok, I use file flag to stop it, or write function to product a file that contains stop flag. `flag=$(echo file_content)`. `if [ flag == 'stop' ] then exit 1` – Kalib Zen Dec 01 '20 at 22:00
0

Suppose you want to run rake test every time you modify any ruby file ("*.rb") in app/ and test/ directories.

Just get the most recent modified time of the watched files and check every second if that time has changed.

Script code

t_ref=0; while true; do t_curr=$(find app/ test/ -type f -name "*.rb" -printf "%T+\n" | sort -r | head -n1); if [ $t_ref != $t_curr ]; then t_ref=$t_curr; rake test; fi; sleep 1; done

Benefits

  • You can run any command or script when the file changes.
  • It works between any filesystem and virtual machines (shared folders on VirtualBox using Vagrant); so you can use a text editor on your Macbook and run the tests on Ubuntu (virtual box), for example.

Warning

  • The -printf option works well on Ubuntu, but do not work in MacOS.
Is Ma
  • 696
  • 8
  • 11