638

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.

Denilson Sá Maia
  • 12,863
  • 12
  • 40
  • 44
  • Possible cross site duplicate of: http://stackoverflow.com/questions/2972765/linux-script-that-monitors-file-changes-within-folders-like-autospec-does ( although here it is on topic =) ) – Ciro Santilli OurBigBook.com Mar 11 '14 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 https://github.com/tartley/rerun2 (or to some other solution without those flaws) – nealmcb Nov 23 '15 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 '21 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). – Denilson Sá Maia Sep 27 '21 at 20:17

41 Answers41

559

Simple, using inotifywait (install your distribution's inotify-tools package):

while inotifywait -e close_write myfile.py; do ./myfile.py; done

or

inotifywait -q -m -e close_write myfile.py |
while read -r filename event; do
  ./myfile.py         # or "./$filename"
done

The first snippet is simpler, but it has a significant downside: it will miss changes performed while inotifywait isn't running (in particular while myfile is running). The second snippet doesn't have this defect. However, beware that it assumes that the file name doesn't contain whitespace. If that's a problem, use the --format option to change the output to not include the file name:

inotifywait -q -m -e close_write --format %e myfile.py |
while read events; do
  ./myfile.py
done

Either way, there is a limitation: if some program replaces myfile.py with a different file, rather than writing to the existing myfile, inotifywait will die. Many editors work that way.

To overcome this limitation, use inotifywait on the directory:

inotifywait -e close_write,moved_to,create -m . |
while read -r directory events filename; do
  if [ "$filename" = "myfile.py" ]; then
    ./myfile.py
  fi
done

Alternatively, use another tool that uses the same underlying functionality, such as incron (lets you register events when a file is modified) or fswatch (a tool that also works on many other Unix variants, using each variant's analog of Linux's inotify).

Gilles 'SO- stop being evil'
  • 69,786
  • 21
  • 137
  • 178
  • 54
    I've encapsulated all of this (with quite a few bash tricks) in a simple-to-use `sleep_until_modified.sh` script, available at: http://bitbucket.org/denilsonsa/small_scripts/src – Denilson Sá Maia Aug 30 '10 at 00:57
  • 15
    `while sleep_until_modified.sh derivation.tex ; do latexmk -pdf derivation.tex ; done` is fantastic. Thank you. – Rhys Ulerich Dec 15 '11 at 16:49
  • 1
    `inotifywait` does not play well with temporary files. If you save a file with vim (`:w`) you'll get 2 `CREATE` and `DELETE` signals, as well as 2 `MOVE` signals. http://stackoverflow.com/questions/10527936/using-inotify-to-keep-track-of-all-files-in-a-system – puk May 10 '12 at 22:30
  • 6
    `inotifywait -e delete_self` seems to work well for me. – Kos Oct 01 '13 at 11:29
  • Nice one, boostrap 3 require to compile style.less whatever less file was changed :) ```while inotifywait -e close_write less/*; do lessc --verbose less/style.less > css/style.css; done``` – sobi3ch Feb 11 '14 at 12:55
  • 3
    It's simple but has two important issues: Events may be missed (all events in the loop) and initialization of inotifywait is done each time which makes this solution slower for large recursive folders. – Wernight Apr 29 '14 at 13:00
  • Thanks for your script @DenilsonSá. I am using it successfully. Can you add support for blocking on multiple files? This would allow such neat use cases as: `while sleep_until_modified.sh *.tex; do pdflatex main.tex; done` – Heinrich Hartmann Nov 04 '14 at 09:44
  • @HeinrichHartmann that's exactly what it already does. – jcoffland Dec 19 '14 at 21:40
  • 1
    That script works well, but it doesn't work with symlinks. – z0r May 23 '15 at 04:56
  • This is great, unfortunately I can't install those tools. I wanted to share [this answer](http://superuser.com/questions/181517/how-to-execute-a-command-whenever-a-file-changes/1056022#1056022) just using watch. – Sebastian Mar 22 '16 at 14:58
  • 12
    For some reason `while inotifywait -e close_write myfile.py; do ./myfile.py; done` always exits without running the command (bash and zsh). For this to work I needed to add `|| true`, eg: `while inotifywait -e close_write myfile.py || true; do ./myfile.py; done` – ideasman42 Aug 03 '16 at 23:36
  • #4 (in [this form](http://pastebin.com/JYgwQ8hw) does not work for me. After saving the watched file (in `gedit`) it just terminates. If I add `|| true` like @ideasman42 proposed, I get `./ CREATE .goutputstream-XXXXXX` but no `echo`. (Initial output looks fine: `Setting up watches. Watches established.` – Raphael Jan 18 '17 at 09:18
  • 1
    I've noticed editors (vim, emacs, geany, gedit...) write files differently, in some cases not triggering an update - basically you need to tweak the `inotifywait` command to match the operations your editor performs when saving. – ideasman42 Jan 18 '17 at 11:40
  • @ideasman42 Yup. See e.g. http://unix.stackexchange.com/questions/188873/using-inotifywait-along-with-vim/188954#188954 and http://unix.stackexchange.com/questions/36467/why-inode-value-changes-when-we-edit-in-vi-editor/37177#37177 – Gilles 'SO- stop being evil' Jan 18 '17 at 12:33
  • Thanks! For everyone , small script to enjoy auto-compilation (of latex or anything) ready to go: https://gist.github.com/gwpl/fd1420d73480329d4e8e4e332d04535c (also tweet to easy re-sharing : https://twitter.com/GWierzowiecki/status/864619834201583616 ) – Grzegorz Wierzowiecki May 16 '17 at 23:19
  • The third option (directory monitoring) needs the -m flag to continue monitoring changes. (I can't suggest the edit since it's only 3 characters which is under the SO minimum edit size.) – burkemw3 Jul 27 '17 at 22:19
  • `echo 'function oc() { inotifywait -e close_write,moved_to,create -m . | while read -r directory events filename; do if [ "$filename" = "$1" ]; then $2; fi; done; }' >> ~/.bashrc; source ~/.bashrc` then `oc myfile.py ./myfile.py` – Ahmed Elsawy Aug 05 '17 at 18:19
  • 1
    For me it’s `while inotifywait -e close_write,moved_to,create .; do asciidoctor "Various proposals.adoc"; done` that works (`-m` hurts). – Olivier Cailloux Apr 04 '18 at 08:40
  • I could not make --exclude work with -r, no matter which methods I tried. – rjurney Jul 31 '18 at 02:55
  • Would like to point out that `inotify` is a Linux kernel module feature that is being used by the userspace executable like `inotifywait`. Without this kernel module loaded or enabled, `inotifywait` won't work. – daparic Sep 12 '18 at 05:43
  • Given the issue with editors replacing files, and the fact that monitoring the directory is often too broad, again because of editors often storing temporary files there, I find this the simplest and most effective solution: `while true; do inotifywait -q myfile.md ; darkslide myfile.md ; done` – Yves Dorfsman Mar 01 '20 at 02:11
  • 1
    More robust version of your last sample: `inotifywait -q --format '%f' -e close_write,moved_to -m . | while IFS= read -r f; do if [ "$f" = "myfile.py" ]; then ./myfile.py; fi; done` Can you update your answer? – Yeti Jun 16 '20 at 07:49
  • 4
    Update: the URL mentioned in the first comment has moved: https://github.com/denilsonsa/small_scripts/blob/master/sleep_until_modified.sh – Denilson Sá Maia Jul 01 '20 at 20:19
  • @DenilsonSáMaia sorry I meant to reply that I found out the issue, and it's not an issue with your script. In my use case, an external process modifies the file without ever closing it (contrary to a user editing a file manually), so I had to add the "modify" option to your script's ```inotifywait -q -m -e``` and ```sed --regexp-extended``` lines, and then it worked smoothly. Am I right, though, that your script it will miss changes performed while ```inotifywait``` isn't running, similar to the first example in the original answer? – Guillaume Perrault-Archambault Nov 26 '20 at 00:24
  • vim sends multiple messages that get captured by solution 3. I added a break after the call to the python script, so as to only execute the payload once per edit. – jwrush Dec 03 '21 at 20:00
  • can `inotifywait ` used in macOS? – alper Jun 02 '22 at 12:37
  • @Gilles'SO-stopbeingevil' may I ask, how do I handle if the file is large and is still being transferred when the event is detected? ie an upstream process sftp a file into my monitored directory. What event should I monitor to ensure that I only process the file when the sftp is completely done? – experiment unit 1998X Jun 12 '23 at 06:30
257

entr (https://github.com/eradman/entr) provides a more friendly interface to inotify (and also supports *BSD & Mac OS X).

It makes it very easy to specify multiple files to watch (limited only by ulimit -n), takes the hassle out of dealing with files being replaced, and requires less bash syntax:

$ find . -name '*.py' | entr ./myfile.py

I've been using it on my entire project source tree to run the unit tests for the code I'm currently modifying, and it's been a huge boost to my workflow already.

Flags like -c (clear the screen between runs) and -d (exit when a new file is added to a monitored directory) add even more flexibility, for example you can do:

$ while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done

As of early 2018 it's still in active development and it can be found in Debian & Ubuntu (apt install entr); building from the author's repo was pain-free in any case.

wesanyer
  • 103
  • 4
Paul Fenney
  • 2,701
  • 1
  • 15
  • 6
  • 10
    Doesn't handle new files and their modifications. – Wernight Apr 29 '14 at 12:58
  • 3
    @Wernight - as of 7th May 2014 entr has the new `-d` flag; it's slightly more long-winded, but you can do `while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done` to deal with new files. – Paul Fenney May 23 '14 at 08:27
  • With the ```-d``` option it is then possible to miss some changes. – jcoffland Dec 19 '14 at 21:39
  • Can it be used in Windows with msysgit or mingw64 ? – Ibn Saeed Apr 21 '15 at 07:56
  • @IbnSaeed I doubt it, as it depends on kernel features to know about file changes (inotify on Linux, kqueue on BSD/OSX). http://stackoverflow.com/questions/3517460/is-there-anything-like-inotify-on-windows may be of interest to you? – Paul Fenney Apr 22 '15 at 13:37
  • For some reason, `entr` only runs once for each file that it's supposed to watch. After that, any modification to that file does not cause a refresh. I can modify another file, and `entr` updates once, but then stops working again... Switched to `inotify` and it works without a problem. – ostrokach Jul 11 '15 at 02:38
  • @ostrokach which version / platform are you using? I've never encountered this problem myself (using various 2.x versions on Linux.) I haven't tried any 3.x version yet though. – Paul Fenney Jul 13 '15 at 08:37
  • 2
    entr is also available in debian repos at least from debian jessie/8.2 on... – Peter V. Mørch Oct 18 '15 at 21:59
  • 8
    It is worth noting that **entr** is available on Homebrew, so `brew install entr` will work as expected – jmarceli Dec 14 '17 at 15:42
  • 1
    **beware** that entr doesn't respond to `ATTRIB` events. – jchook Jun 22 '18 at 01:44
  • sometimes you might want to add `python` before the python script name, in those commands above.. – matt Jun 28 '18 at 16:40
  • I've had a problem with entr because it requires the keyboard (q is quit, space does something else). I wanted to have entr running in the background to generate an output file from an input file and then start vim on the input file for the user to edit. But vim locks up. I think this is because entr grabs the input. – artfulrobot Jul 05 '18 at 14:53
  • How do you check what files are currently tracked by entr and is entr persistent after restart of the system? No sites comments on this or how to remove tracked files again. – Cristian Matthias Ambæk Dec 16 '19 at 09:19
  • The list of tracked files doesn't change, its passed into `entr` at the start. To add/remove files you stop and restart `entr` with a new list. The `-d` option makes `entr` exit automatically when it detects any change, so you can run your `find` command again (because `entr` won't try to guess whether the affected file is of interest). The `while` loop example in the answer above works well. `entr` does not persist after a restart. – Paul Fenney Dec 17 '19 at 10:48
  • Use silversearcher `-l` flag handily ignores things like the .git directory. So `ag -l | fd` is pretty convenient. – GcL Jun 10 '20 at 16:27
147

I wrote a Python program to do exactly this called when-changed.

Usage is simple:

when-changed FILE COMMAND...

Or to watch multiple files:

when-changed FILE [FILE ...] -c COMMAND

FILE can be a directory. Watch recursively with -r. Use %f to pass the filename to the command.

joh
  • 1,675
  • 1
  • 11
  • 3
  • does it work when the file is replaced like the editors do? – Janus Troelsen Oct 11 '12 at 12:57
  • 1
    @ysangkok yes it does, in the latest version of the code :) – joh Oct 11 '12 at 16:35
  • +1, very nice. ruby has `watchr` but this is more generic in that it works for bash commands. Perhaps you could add a flag that lets it mirror `watch` of bash, i.e clear the screen each time, and perhaps show last executed time – Karthik T Oct 20 '13 at 07:04
  • Awesome utility - I created something similar for windows: https://github.com/benblamey/when_changed – Ben Jul 14 '14 at 14:08
  • 6
    Now available from "pip install when-changed". Still works nicely. Thanks. – A. L. Flanagan Jan 08 '15 at 18:37
  • 2
    To clear the screen first you can use `when-changed FILE 'clear; COMMAND'`. – Dave James Miller Feb 14 '15 at 09:29
  • Can't get it to work. I install it with pip, but it's never found in the path – coyotte508 Apr 27 '15 at 08:00
  • 1
    This answer is so much better because I can do it on Windows, too. And this guy actually wrote a program to get the answer. – Wolfpack'08 May 13 '15 at 15:33
  • Every time I modify a file, `when-changed` runs the specified command 3 times. I find `inotify` to be the best tool out of the three. – ostrokach Jul 11 '15 at 02:35
  • @ostrokach: Interesting, that may be a bug. Could you please [file an issue with more details on GitHub](https://github.com/joh/when-changed/issues/new)? – joh Jul 22 '15 at 07:10
  • This interprets your given command using 'sh', so it will behave differently than if you typed the command at a terminal (where I assume you're using a different shell). For example, if the command contains .bashrc aliases like `ll`, it won't work. – Jonathan Hartley Sep 09 '15 at 20:43
  • Does this support windows ? – Stuart Axon Oct 23 '15 at 15:40
  • 5
    Good news everyone! `when-changed` is now cross-platform! Check out the latest [0.3.0 release](https://github.com/joh/when-changed/releases/tag/v0.3.0) :) – joh Jan 23 '16 at 02:41
  • 1
    I tried when_changed and every time I save a modified file I get 2 events!... It can't find the function I just defined in the script too. Also, I need to run command as root and it doesn't seem to work at all. – Shautieh Mar 09 '16 at 06:19
  • @Shautieh: How do you run `when-changed`? Try running with `-v` to get an idea of why 2 events are triggered. Quite possibly your editor creates one or more temporary files when saving. – joh Aug 29 '16 at 15:55
  • @joh I would need to test again as I dumped `when-changed` months ago as a result. It may really well come from my editor (I'm using lighttable), but I think that this script should handle these cases more gracefully (like, ignore similar subsequent events within a 500ms period or something). – Shautieh Aug 30 '16 at 00:41
  • 1
    Superperfect solution, expecially if paired with `latexrun` to run pdflatex on your file the right number of times. – linello Oct 17 '16 at 12:27
  • Great app. BUGFIX: script fires twice on file save – PowerAktar Jun 01 '23 at 12:38
72

How about this script? It 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

### Set initial time of file
LTIME=`stat -c %Z /path/to/the/file.txt`

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

   if [[ "$ATIME" != "$LTIME" ]]
   then    
       echo "RUN COMMAND"
       LTIME=$ATIME
   fi
   sleep 5
done
Pablo A
  • 1,470
  • 13
  • 21
VDR
  • 1,061
  • 9
  • 8
  • 4
    Wouldn't `stat`-ing the modified time be a better "whenever a file changes" answer? – Xen2050 Feb 27 '16 at 16:42
  • 1
    Would running stat many times per second cause many reads to the disk? or would the fstat system call automatically make cache these responses somehow? I'm trying to write a sort of 'grunt watch' to compile my c code whenever I make changes – Oskenso Kashi Dec 07 '16 at 18:01
  • This is good if you know the filename to be watched in advance. Better would be to pass the filename to the script. Better still would be if you could pass many filenames (eg. "mywatch *.py"). Better still would be if it could operate recursively on files in subdirs too, which some of the other solutions do. – Jonathan Hartley Feb 16 '17 at 14:18
  • 9
    Just in case anyone is wondering about heavy reads, I tested this script in Ubuntu 17.04 with a sleep of 0.05s and ```vmstat -d``` to watch for disk access. It seems linux does a fantastic job at caching this sort of thing :D – Oskenso Kashi Aug 02 '17 at 00:01
  • Looks like `stat -f %m /path/to/the/file.txt` is the MacOS equivalent. – ɲeuroburɳ Aug 24 '22 at 17:39
48

For those who can't install inotify-tools like me, this should be useful:

watch -d -t -g ls -lR

This command will exit when the output changes, ls -lR will list every file and directory with its size and dates, so if a file is changed it should exit the command, as man says:

-g, --chgexit
          Exit when the output of command changes.

I know this answer may not be read by anyone, but I hope someone would reach to it.

Command line example:

~ $ cd /tmp
~ $ watch -d -t -g ls -lR && echo "1,2,3"

Open another terminal:

~ $ echo "testing" > /tmp/test

Now the first terminal will output 1,2,3

Simple script example:

#!/bin/bash
DIR_TO_WATCH=${1}
COMMAND=${2}

watch -d -t -g ls -lR ${DIR_TO_WATCH} && ${COMMAND}
Sebastian
  • 591
  • 4
  • 3
  • 10
    Nice hack. I tested and it seems to have a problem when the listing is long and the changed file falls outside the screen. A small modification could be something like this: `watch -d -t -g "ls -lR tmp | sha1sum"` – Atle Dec 16 '16 at 14:10
  • 3
    if you watch your solution every second, it works forever and run MY_COMMAND only if some file changes: watch -n1 "watch -d -t -g ls -lR && MY_COMMAND" – mnesarco Jul 23 '17 at 02:26
  • My version of watch (On Linux, `watch from procps-ng 3.3.10`) accepts float seconds for its interval, hence `watch -n0.2 ...` will poll every fifth of a second. Good for those healthy sub-millisecond unit tests. – Jonathan Hartley Apr 19 '18 at 16:23
  • this does not seem to be in a loop though, and ends up after the first iteration – FantomX1 Nov 04 '20 at 22:08
  • @FantomX1 For a one-liner or handy alias, something like this should work: `while [[ 1=1 ]] ; do watch -n 5 -g 'ls -lrt --full-time ./Dockerfile' && docker build . ; sleep 6 ; done` or `while [[ 1=1 ]] ; do watch -n 5 -g 'ls -lrt --full-time *.c *.h ' && make clean && make ; sleep 6 ; done` – Justin Jan 11 '21 at 21:44
  • So close! The problem with watch is that it sets exit code to 0 when ctrl c out of it, so when in a loop you get strange behavior – pmiguelpinto90 Sep 23 '21 at 22:44
  • 2
    The command line I end finding useful is: `watch -n1 "watch -t -g ls --full-time >/dev/null && MY_CMD"` Note the --full-time otherwise the timestamp is only to the minute, and the redirect of the watch-ls output to dev null. – TvE Dec 25 '21 at 19:18
  • @TvE how is that different to `watch -t -g ls --full-time >/dev/null && MY_CMD`? In my case, `MY_CMD` is `cat a.js b.js > c.js`, and without `watch -n1` part, `MY_CMD` doesn't get executed. And why do I need `>/dev/null` part? I tested without it and `MY_CMD` got called without it. – Changdae Park Feb 19 '22 at 12:59
  • @ChangdaePark: the inner watch command waits for a file to change and then MY_CMD runs. The outer watch repeats the whole process over and over. The >/dev/null is 'cause you don't want to see the output of the ls command, the ls is there only so watch can tell when a file changed. – TvE Feb 21 '22 at 06:21
  • How does one "fix" the issue with non-working Ctrl+C when using the `while true; do` loop approach? – Paweł Gościcki Jul 27 '22 at 09:35
34

Solution using Vim:

:au BufWritePost myfile.py :silent !./myfile.py

But I don't want this solution because it's kinda annoying to type, it's a bit hard to remember what to type, exactly, and it's a bit difficult to undo its effects (need to run :au! BufWritePost myfile.py). In addition, this solution blocks Vim until the command has finished executing.

I've added this solution here just for completeness, as it might help other people.

To display the program output (and completely disrupt your editting flow, as the output will write over your editor for a few seconds, until you press Enter), remove the :silent command.

Denilson Sá Maia
  • 12,863
  • 12
  • 40
  • 44
  • 1
    This can be quite nice when combined with `entr` (see below) - just make vim touch a dummy file that entr is watching, and let entr do the rest in the background... or `tmux send-keys` if you happen to be in such an environment :) – Paul Fenney May 23 '14 at 08:42
  • nice! you can make a macro for your `.vimrc` file – ErichBSchulz Mar 19 '18 at 01:49
33

If you happen to have npm installed, nodemon is probably the easiest way to get started, especially on OS X, which apparently doesn't have inotify tools. It supports running a command when a folder changes.

Eric Leschinski
  • 6,828
  • 6
  • 45
  • 51
davidtbernal
  • 474
  • 5
  • 8
  • 5
    However, it only watches .js and .coffee files. – zelk Jul 20 '12 at 10:27
  • 7
    The current version seems to support any command, for example: `nodemon -x "bundle exec rspec" spec/models/model_spec.rb -w app/models -w spec/models` – kek Apr 10 '13 at 15:15
  • 1
    I wish I had more info, but osx does have a method to track changes, fsevents – ConstantineK Oct 07 '14 at 04:34
  • 1
    On OS X you can also use [Launch Daemons](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) with a `WatchPaths` key as shown in my link. – Adam Johns Nov 16 '14 at 23:48
31

rerun2 (on github) is a 10-line Bash script of the form:

#!/usr/bin/env bash

function execute() {
    clear
    echo "$@"
    eval "$@"
}

execute "$@"

inotifywait --quiet --recursive --monitor --event modify --format "%w%f" . \
| while read change; do
    execute "$@"
done

Save the github version as 'rerun' on your PATH, and invoke it using:

rerun COMMAND

It runs COMMAND every time there's a filesystem modify event within your current directory (recursive.)

Things one might like about it:

  • It uses inotify, so is more responsive than polling. Fabulous for running sub-millisecond unit tests, or rendering graphviz dot files, every time you hit 'save'.
  • Because it's so fast, you don't have to bother telling it to ignore large subdirs (like node_modules) just for performance reasons.
  • It's extra super responsive, because it only calls inotifywait once, on startup, instead of running it, and incurring the expensive hit of establishing watches, on every iteration.
  • It's just 12 lines of Bash
  • Because it's Bash, it interprets commands you pass it exactly as if you had typed them at a Bash prompt. (Presumably this is less cool if you use another shell.)
  • It doesn't lose events that happen while COMMAND is executing, unlike most of the other inotify solutions on this page.
  • On the first event, it enters a 'dead period' for 0.15 seconds, during which other events are ignored, before COMMAND is run exactly once. This is so that the flurry of events caused by the create-write-move dance which Vi or Emacs does when saving a buffer don't cause multiple laborious executions of a possibly slow-running test suite. Any events which then occur while COMMAND is executing are not ignored - they will cause a second dead period and subsequent execution.

Things one might dislike about it:

  • It uses inotify, so won't work outside of Linuxland.
  • Because it uses inotify, it will barf on trying to watch directories containing more files than the max number of user inotify watches. By default, this seems to be set to around 5,000 to 8,000 on different machines I use, but is easy to increase. See https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached
  • It fails to execute commands containing Bash aliases. I could swear that this used to work. In principle, because this is Bash, not executing COMMAND in a subshell, I'd expect this to work. I'd love to hear If anyone knows why it doesn't. Many of the other solutions on this page can't execute such commands either.
  • Personally I wish I was able to hit a key in the terminal it's running in to manually cause an extra execution of COMMAND. Could I add this somehow, simply? A concurrently running 'while read -n1' loop which also calls execute?
  • Right now I've coded it to clear the terminal and print the executed COMMAND on each iteration. Some folks might like to add command-line flags to turn things like this off, etc. But this would increase size and complexity many-fold.

This is a refinement of @cychoi's anwer.

Jonathan Hartley
  • 1,006
  • 13
  • 25
  • 2
    I believe you should use `"$@"` instead of `$@`, in order to properly work with arguments containing spaces. But at the same time you use `eval`, which forces the user of rerun to be extra careful when quoting. – Denilson Sá Maia Sep 10 '15 at 15:20
  • Thanks Denilson. Could you given an example of where quoting needs to be done carefully? I've been using it the last 24 hours and haven't seen any problems with spaces thus far, nor *carefully* quoted anything - just invoked as `rerun 'command'`. Are you just saying that if I used "$@", then the user could invoke as `rerun command` (without quotes?) That doesn't seem as useful for me: I generally don't want Bash to do *any* processing of command before passing it to rerun. e.g. if command contains "echo $myvar", then I'll want to see the new values of myvar in each iteration. – Jonathan Hartley Sep 11 '15 at 17:53
  • 1
    Something like `rerun foo "Some File"` might break. But since you are using `eval`, it can be rewritten as `rerun 'foo "Some File"`. Note that sometimes the path expansion might introduce spaces: `rerun touch *.foo` will likely break, and using `rerun 'touch *.foo'` has slightly different semantics (path expansion happening only once, or multiple times). – Denilson Sá Maia Sep 11 '15 at 23:07
  • Thanks for the help. Yep: `rerun ls "some file"` breaks because of the spaces. `rerun touch *.foo*` works fine usually, but fails if the filenames that match \*.foo contain spaces. Thanks for helping me see how `rerun 'touch *.foo'` has different semantics, but I suspect the version with single quotes is the semantic I want: I want each iteration of rerun to act as if I typed the command over again - hence I *want* `*.foo` to be expanded on each iteration. I'll try your suggestions to examine their effects... – Jonathan Hartley Sep 12 '15 at 05:52
  • More discussion on this PR (https://github.com/tartley/rerun2/pull/1) and others. – Jonathan Hartley Sep 12 '15 at 06:09
19

if you have nodemon installed, then you can do this:

nodemon -w <watch directory> -x "<shell command>" -e ".html"

In my case I edit html locally and ship it to my remote server when a file changes.

nodemon -w <watch directory> -x "scp filename jaym@jay-remote.com:/var/www" -e ".html"
Jay
  • 291
  • 2
  • 4
15

Here's a simple shell Bourne shell script that:

  1. Takes two arguments: the file to be monitored and a command (with arguments, if necessary)
  2. Copies the file you are monitoring to the /tmp directory
  3. Checks every two seconds to see if the file you are monitoring is newer than the copy
  4. If it's newer it overwrites the copy with the newer original and executes the command
  5. Cleans up after itself when you press Ctr-C

    #!/bin/sh  
    f=$1  
    shift  
    cmd=$*  
    tmpf="`mktemp /tmp/onchange.XXXXX`"  
    cp "$f" "$tmpf"  
    trap "rm $tmpf; exit 1" 2  
    while : ; do  
        if [ "$f" -nt "$tmpf" ]; then  
            cp "$f" "$tmpf"  
            $cmd  
        fi  
        sleep 2  
    done  
    

This works on FreeBSD. The only portability issue I can think of is if some other Unix doesn't have the mktemp(1) command, but in that case you can just hard code the temp file name.

Gilles 'SO- stop being evil'
  • 69,786
  • 21
  • 137
  • 178
MikeyMike
  • 159
  • 3
  • 9
    Polling is the only portable way, but most systems have a file change notification mechanism (inotify on Linux, kqueue on FreeBSD, ...). You have a severe quoting problem when you do `$cmd`, but fortunately that's easily fixable: ditch the `cmd` variable and execute `"$@"`. Your script is not suitable for monitoring a large file, but that could be fixed by replacing `cp` by `touch -r` (you only need the date, not the contents). Portability-wise, the `-nt` test requires bash, ksh or zsh. – Gilles 'SO- stop being evil' Aug 27 '10 at 22:22
  • 1
    I'm curious, how this compares with https://superuser.com/a/634313 Is creating a copy more portable than just `stat` on the file? – CervEd Jan 08 '22 at 12:53
11

Have a look at incron. It's similar to cron, but uses inotify events instead of time.

Florian Diesch
  • 3,780
  • 1
  • 19
  • 16
  • This can be made to work, but creating an incron entry is quite a labour intensive process compared to other solutions on this page. – Jonathan Hartley Sep 09 '15 at 20:37
  • This project was abandoned, then [forked on GitHub](https://github.com/ar-/incron) (several Linux distributions are using this fork), then abandoned again… – Franklin Yu Jun 09 '22 at 03:58
8

Under Linux:

man watch

watch -n 2 your_command_to_run

Will run the command every 2 seconds.

If your command takes more than 2 seconds to run, watch will wait until it's done before doing it again.

Eric Leschinski
  • 6,828
  • 6
  • 45
  • 51
  • That's pretty simple, though somewhat of a waste, it's easy for development tasks like making live changes to styles. – Xeoncross Jul 15 '14 at 18:32
  • @thirtythreeforty A quick experiment on Ubuntu shows that watch will wait the full two seconds no matter how long the command takes to run. FWIW, the sleep period can be specified with '-n', down to a minimum of 0.1 seconds. – Jonathan Hartley Sep 09 '15 at 20:36
  • This does not answer the question but runs a program periodical. – dirdi Dec 01 '19 at 12:49
8

Another solution with NodeJs, fsmonitor :

  1. Install

    sudo npm install -g fsmonitor
    
  2. From command line (example, monitor logs and "retail" if one log file change)

    fsmonitor -s -p '+*.log' sh -c "clear; tail -q *.log"
    
Kevin Panko
  • 7,346
  • 22
  • 44
  • 53
Atika
  • 199
  • 1
  • 3
8

Improved upon edit#3 of Gilles's answer.

This version runs inotifywait once and monitors for events (.e.g.: modify) thereafter. Such that inotifywait doesn't need to be re-executed upon every event encountered.

It's quick and fast!(even when monitoring large directory recursively)

inotifywait --quiet --monitor --event modify FILE | while read; do
    # trim the trailing space from inotifywait output
    REPLY=${REPLY% }
    filename=${REPLY%% *}
    # do whatever you want with the $filename
done
Greenonline
  • 2,235
  • 11
  • 24
  • 30
cychoi
  • 416
  • 1
  • 5
  • 13
  • 1
    This is the best answer on the page for Linux-only users. Replace the stuff inside the loop with 'execute $@', and the user could call this script passing in their own command to run. It even works with commands that contain shell aliases if you source it, using something like ". scriptname COMMAND". This will still find scriptname on the PATH. – Jonathan Hartley Sep 09 '15 at 21:00
7

Watchdog is a Python project, and may be just what you're looking for:

Supported platforms

  • Linux 2.6 (inotify)
  • Mac OS X (FSEvents, kqueue)
  • FreeBSD/BSD (kqueue)
  • Windows (ReadDirectoryChangesW with I/O completion ports; ReadDirectoryChangesW worker threads)
  • OS-independent (polling the disk for directory snapshots and comparing them periodically; slow and not recommended)

Just wrote a command-line wrapper for it watchdog_exec:

Example runs

On fs event involving files and folders in current directory, run echo $src $dst command, unless it the fs event is modified, then run python $src command.

python -m watchdog_exec . --execute echo --modified python

Using short arguments, and restricting to only execute when events involve "main.py":

python -m watchdog_exec . -e echo -a echo -s __main__.py

EDIT: Just found Watchdog has an official CLI called watchmedo, so check that out also.

Samuel Marks
  • 171
  • 1
  • 1
7

If your program generates some sort of log/output, you can create a Makefile with a rule for that log/output that depends on your script and do something like

while true; do make -s my_target; sleep 1; done

Alternately, you can create a phony target and have the rule for it both call your script and touch the phony target (while still depending on your script).

ctgPi
  • 179
  • 2
  • 11
    `while sleep 1 ; do something ; done` is slightly better than `while true ; do something ; sleep 1 ; done`. At least it stops easily when pressing Ctrl+C. – Denilson Sá Maia Aug 28 '10 at 04:59
  • Will removing the sleep cause a busy loop (CPU generating heat and hurting battery life on a laptop)? – Steven Lu Jul 12 '12 at 04:52
  • 3
    @StevenLu: no, the sleep is not a busy wait. The problem is that if the sleep is in the body, Control-C will kill the sleep and the loop will start over. Power usage of starting the loop over is insignificant. Try it yourself in a terminal. You need to hold Control-C for it to work, if you have sleep in the body. – Janus Troelsen Sep 19 '12 at 11:25
  • Right. I think I missed it and didn't see that the sleep is still present as the loop condition. That little tweak is pretty awesome. – Steven Lu Sep 19 '12 at 17:55
7

Look into Guard, in particular with this plugin:

https://github.com/hawx/guard-shell

You can set it up to watch any number of patterns in your project's directory, and execute commands when changes occur. Good chance even that there's a plugin available for that what you're trying to do in the first place.

6

I like the simplicity of while inotifywait ...; do ...; done however it has two issues:

  • File changes happening during the do ...; will be missed
  • Slow when using in recursive mode

Therefor I made a helper script that uses inotifywait without those limitations: inotifyexec

I suggest you put this script in your path, like in ~/bin/. Usage is described by just running the command.

Example: inotifyexec "echo test" -r .

Wernight
  • 621
  • 1
  • 8
  • 13
5

Improved Sebastian's solution with watch command:

watch_cmd.sh:

#!/bin/bash
WATCH_COMMAND=${1}
COMMAND=${2}

while true; do
  watch -d -g "${WATCH_COMMAND}"
  ${COMMAND}
  sleep 1     # to allow break script by Ctrl+c
done

Call example:

watch_cmd.sh "ls -lR /etc/nginx | grep .conf$" "sudo service nginx reload"

It works but be careful: watch command has known bugs (see man): it reacts on changes only in VISIBLE in terminal parts of -g CMD output.

alex_1948511
  • 160
  • 1
  • 4
5

You could try reflex.

Reflex is a small tool to watch a directory and rerun a command when certain files change. It's great for automatically running compile/lint/test tasks and for reloading your application when the code changes.

# Rerun make whenever a .c file changes
reflex -r '\.c$' make
masterxilo
  • 498
  • 7
  • 7
  • Can you quote / explain a little bit about the tool? Have a quick read of [how to recommend software](http://meta.superuser.com/questions/5329/how-do-i-recommend-software-in-my-answers/5330) for guidance. – bertieb Mar 01 '17 at 20:38
5

swarminglogic wrote a script called watchfile.sh, also available as a GitHub Gist.

Denilson Sá Maia
  • 12,863
  • 12
  • 40
  • 44
  • 2
    This is a feature-packed 200 line Bash script that polls `stat` on the given filenames, runs `md5sum` on the output, and re-runs the given command if this value changes. Because it's Bash, I suspect it does a good job of running the given command exactly as if you typed it at a Bash prompt. (In contrast, most of the solutions here written in other languages will fail to execute commands which, for example, contain shell aliases such as `ll`) – Jonathan Hartley Sep 09 '15 at 20:30
4

A little more on the programming side, but you want something like inotify. There are implementations in many languages, such as jnotify and pyinotify.

This library allows you to monitor single files or entire directories, and returns events when an action is discovered. The information returned includes the file name, the action (create, modify, rename, delete) and the file path, among other useful information.

John T
  • 163,373
  • 27
  • 341
  • 348
4

For those of you who are looking for a FreeBSD solution, here is the port:

/usr/ports/sysutils/wait_on
slhck
  • 223,558
  • 70
  • 607
  • 592
akond
  • 246
  • 1
  • 4
3

I use this script to do it. I'm using inotify in monitor-mode

#!/bin/bash
MONDIR=$(dirname $1)
ARQ=$(basename $1)

inotifywait -mr -e close_write $MONDIR | while read base event file 
do
  if (echo $file |grep -i "$ARQ") ; then
    $1
  fi
done

Save this as runatwrite.sh

Usage: runatwrite.sh myfile.sh

it will run myfile.sh at each write.

Archemar
  • 1,687
  • 2
  • 16
  • 20
3

Check out https://github.com/watchexec/watchexec.

watchexec is a simple, standalone tool that watches a path and runs a command whenever it detects modifications.

Example

Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running make when a change is detected:

$ watchexec --exts js,css,html make

Tails
  • 131
  • 2
3

A oneliner answer that I'm using to keep track on a file change:

$ while true ; do NX=`stat -c %Z file` ; [[ $BF != $NX ]] && date >> ~/tmp/fchg && BF=$NX || sleep 2 ; done

You don't need to initialize BF if you know that the first date is the starting time.

This is simple and portable. There is another answer based on the same strategy using a script here. Take a look also.


Usage: I'm using this to debug and keep an eye on ~/.kde/share/config/plasma-desktop-appletsrc; that for some unknown reason keeps loosing my SwitchTabsOnHover=false

DrBeco
  • 1,915
  • 2
  • 17
  • 16
2

For those using OS X, you can use a LaunchAgent to watch a path/file for changes and do something when that happens. FYI - LaunchControl is a good app to easily make/modify/remove daemons/agents.

(example taken from here)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN
http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>test</string>
    <key>ProgramArguments</key>
    <array>
        <string>say</string>
        <string>yy</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>~/Desktop/</string>
    </array>
</dict>
</plist>
Hefewe1zen
  • 1,824
  • 15
  • 16
2

I have a GIST for this and the usage is pretty simple

watchfiles <cmd> <paths...>

https://gist.github.com/thiagoh/5d8f53bfb64985b94e5bc8b3844dba55

thiagoh
  • 151
  • 5
2

As a few others have done, I've also written a lightweight command line tool to do this. It's fully documented, tested and modular.

Watch-Do

Installation

You can install it (if you have Python3 and pip) using:

pip3 install git+https://github.com/vimist/watch-do

Usage

Use it straight away by running:

watch-do -w my_file -d 'echo %f changed'

Features Overview

  • Supports file globbing (use -w '*.py' or -w '**/*.py')
  • Run multiple commands on a file change (just specify the -d flag again)
  • Dynamically maintains the list of files to watch if globbing is used (-r to turn this on)
  • Multiple ways to "watch" a file:
    • Modification time (default)
    • File hash
    • Trivial to implement your own (this is the ModificationTime watcher)
  • Modular design. If you want to have commands run, when a file is accessed, it's trivial to write your own watcher (mechanism that determines if the doers should be run).
OdinX
  • 191
  • 8
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.)

Jonathan Hartley
  • 1,006
  • 13
  • 25
  • This answer is not remotely as good as my other answer (rerun2) – Jonathan Hartley Sep 09 '15 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"` – Vladimir Vukanac Feb 25 '20 at 20:31
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.

William Entriken
  • 2,224
  • 5
  • 27
  • 39
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
Pablo A
  • 1,470
  • 13
  • 21
Leon S.
  • 134
  • 4
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")
Bluu
  • 206
  • 2
  • 5
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.

LondonRob
  • 405
  • 7
  • 27
  • 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. – Jonathan Hartley Sep 09 '15 at 19:51
0

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

  • Please read [How do I recommend software](https://meta.superuser.com/questions/5329/how-do-i-recommend-software-in-my-answers/5330#5330) 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 '17 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.

petermeissner
  • 453
  • 4
  • 8
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.

lvijay
  • 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`). – Denilson Sá Maia Jan 29 '19 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
yunzen
  • 127
  • 8
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
Yeti
  • 161
  • 9
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.

Denilson Sá Maia
  • 12,863
  • 12
  • 40
  • 44