52

I am trying to create a bash script with 2 parameters:

  • a directory
  • a command.

I want to watch the directory parameter for changes: when something has been changed the script should execute the command.

I'm running MacOS, not Linux; any pointers or external resources would greatly help as I have see that this is difficult to achieve. Really OI am trying to mimic SASS's watch functionality.

#!/bin/bash

#./watch.sh $PATH $COMMAND

DIR=$1  

ls -l $DIR > $DIR/.begin
#this does not work
DIFFERENCE=$(diff .begin .end)

if [ $DIFFERENCE = '\n']; then
    #files are same
else
    $2
fi 

ls -l $DIR > $DIR/.end
Scott Anderson
  • 631
  • 5
  • 26
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424

13 Answers13

48

To continuously recursively monitor folder (md5) and execute a command on change:

daemon() {
    chsum1=""

    while [[ true ]]
    do
        chsum2=`find src/ -type f -exec md5 {} \;`
        if [[ $chsum1 != $chsum2 ]] ; then           
            if [ -n "$chsum1" ]; then
                compile
            fi
            chsum1=$chsum2
        fi
        sleep 2
    done
}

Works on my OS X as I do not have digest.

On Linux, you can use md5sum as a replacement for the md5 command.

jv-k
  • 618
  • 5
  • 20
Radek
  • 3,913
  • 3
  • 42
  • 37
  • 4
    Nice, I would however suggest replacing `chsum1=...` with `chsum1=$chsum2`. Otherwise, changes happening during `compile` would not be noticed. – Kleist Apr 08 '12 at 09:45
  • 4
    There is "fswatch" for OS X, here: https://github.com/alandipert/fswatchIt It is a small command that uses FSEvents API, so it does the same thing, but saves your CPU. It's better for large projects (no need to MD5 everything). – oshyshko Nov 10 '13 at 09:16
  • 4
    +1. little remark, `find src/ -type f -mtime -5s` is significantly faster and requires little too no cpu since it doesn't exec on each file. it checks if anything changed past 5 seconds. – Devrim Jul 16 '14 at 19:52
  • @Devrim Nice! This doesn't find deletes but works for my purposes. – crizCraig Mar 18 '15 at 20:03
  • @devrim My 'find' doesn't seem to like '-5s' as an argument for mtime. It will accept an int or float (number of days). I'm using Gnu find v4.4.2, which is the current on Ubuntu 15.04. Am I doing it wrong? – Jonathan Hartley Sep 08 '15 at 20:50
  • @devrim Doesn't specifying a 'modified in last few seconds' like this not work properly, because it executes the command more than once? – Jonathan Hartley Sep 08 '15 at 20:51
  • @Devrim - `"$(find ./ -type f -mmin -5 -exec md5sum {} \;)"` isn't 5s but it doesn't throw an error. – jgraup Oct 04 '16 at 22:39
  • 1
    rather than hashing the file everytime, just diff the timestamps `ls --full-time $file | awk '{ print $7 }'`; I like this implementation > https://stackoverflow.com/a/25869844/177389 better. – qodeninja Sep 29 '19 at 18:52
17

I can’t believe nobody posted this yet.

First make sure inotify-tools are installed.

Then use them like this:

logOfChanges="/tmp/changes.log.csv" # Set your file name here.

# Lock and load
inotifywait -mrcq $DIR > "$logOfChanges" &
IN_PID=$$

# Do your stuff here
...

# Kill and analyze
kill $IN_PID
cat "$logOfChanges" | while read entry; do
   # Split your CSV, but beware that file names may contain spaces too.
   # Just look up how to parse CSV with bash. :)
   path=... 
   event=...
   ...  # Other stuff like time stamps?
   # Depending on the event…
   case "$event" in
     SOME_EVENT) myHandlingCode path ;;
     ...
     *) myDefaultHandlingCode path ;;
done

Alternatively, using --format instead of -c on inotifywait would be an idea.

Just man inotifywait and man inotifywatch for more infos.

Evi1M4chine
  • 6,992
  • 1
  • 24
  • 18
  • 2
    Just tried installing `inotify-tools` on a mac and it failed, posted [github issue](https://github.com/rvoicilas/inotify-tools/issues/11) if your interested. – ThomasReggi Aug 01 '12 at 04:07
  • I posted a solution for your github issue. It’s only an autoconf version mismatch. :) – Evi1M4chine Aug 21 '12 at 14:00
  • 4
    I note that `inotify-tools` needs a Linux kernel supporting `inotify`. Unless you know something about Mac OS X and `inotify` that no-one else does, there is no direct `inotify` support (but the `FSEvents` API — File System Events — will get close to the functionality needed, even if the interface is different). – Jonathan Leffler Sep 04 '13 at 22:40
  • 2
    @JonathanLeffler: `fswatch`, which uses `FSEvents`, seems to be a nice full-featured choice: http://stackoverflow.com/questions/1515730/is-there-a-command-like-watch-or-inotifywait-on-the-mac – Evi1M4chine Jan 30 '16 at 11:30
15

Here's an example of watching a folder for changes and running a the less compiler when one is updated. As a prereq you need npm and these the module onchange. The node community has a whole bunch of different watch commands (like onchange) I'm not aware of any that are compiled self-contained binaries.

npm install less onchange -g

Then you can use something like:

onchange "./stylesheets/*.less" -- lessc main.less > main.css

I prefer a BASH command over the Grunt answer I gave a while back.

Community
  • 1
  • 1
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424
  • 1
    because your question directly implies bash I would expect the answer to be bash-focused rather than npm. I strongly recommend the bash answer with the higher upvotes be chosen instead. – qodeninja Sep 29 '19 at 18:37
12

METHOD 1:

#!/bin/sh

check() {
    dir="$1"
    chsum1=`digest -a md5 $dir | awk '{print $1}'`
    chsum2=$chsum1

    while [ $chsum1 -eq $chsum2 ]
    do
        sleep 10
        chsum2=`digest -a md5 $dir | awk '{print $1}'`
    done

    eval $2
}

check $*

This script takes in two parameters [directory, command]. Every 10 seconds the script executes check() to see it the folder has changed. If not it sleeps and the cycle repeats.

In the event that the folder has changed, it evals your command.

METHOD 2:
Use a cron to monitor the folder.

You'll have to install incron:

 sudo apt-get install incron

And then your script will look something like this:

#!/bin/bash
eval $1

(You won't need to specify the folder since it will be the job of the cron to monitor the specified directory)

A full, working example can be found here:

http://www.errr-online.com/index.php/2011/02/25/monitor-a-directory-or-file-for-changes-on-linux-using-inotify/

David Xia
  • 5,075
  • 7
  • 35
  • 52
Swift
  • 13,118
  • 5
  • 56
  • 80
7

probably the fastest way of doing it.. (on 1G git repo, returns under 1sec.)

#!/bin/bash

watch() {

    echo watching folder $1/ every $2 secs.

while [[ true ]]
do
    files=`find $1 -type f -mtime -$2s`
    if [[ $files == "" ]] ; then
        echo "nothing changed"
    else
            echo changed, $files
    fi
    sleep $2
done
}

watch folder 3
Devrim
  • 2,826
  • 5
  • 25
  • 31
  • 1
    thanks for the comment. future readers: when i posted this answer there were no other alternatives than grunt - now i see there are some answers above that use the same approach but are better; use their solution instead. – Devrim Jun 19 '15 at 13:04
  • 2
    This is pretty short and slick since you don't have to install additional dependencies. One thing it doesn't catch though is deletes; only add/updates. – bigtunacan Oct 16 '18 at 19:14
3

In Mac OS X, you can just control-click a folder, then click 'Folder Actions Setup'. This will allow you attach actions to a folder, i.e. scripts to run.

OS X comes with a number of prebuilt scripts, or you can create your own.

John Vargo
  • 530
  • 3
  • 9
1

Almost 3 years later and I'd recommend this grunt based solution.

I created a working example here https://github.com/reggi/watch-execute.

Here's the Gruntfile.js:

module.exports = function (grunt) {
  grunt.initConfig({
    shell: {
      run_file:{
        command: 'sh ./bash.sh',
        options: {
            stdout: true
        }
      }
    },
    watch: {
      run_file: {
        files: ["./watchme/*"],
        tasks: ["shell:run_file"]
      }
    }
  });
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-shell');
};
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424
1

I wrote a general utility called watchfile for simplifying these kinds of operations.

It is less powerfull than inotifywatch, but I prefer a simpler, less verbose utility.

For the desired task, you want to monitor if any files in current directory have been modified. To list all files recursively in the current directory:

find . -type f

To output the timestamp information of each of these files:

find . -type f -print0 | xargs -0 stat

Now, you can monitor this output with the watchfile utility and execute a command CMD when this information changes:

watchfile -s "find . -type f -print0 | xargs -0 stat" -e CMD
swalog
  • 4,403
  • 3
  • 32
  • 60
1
#!/bin/bash

# Author: Devonte
# NGINX WATCH DAEMON
# Place file in root of nginx folder /etc/nginx
# This will test your nginx config on any change and
# if there are no problems it will reload your configuration
# USAGE: sh nginx-watch.sh

dir=`dirname $0`

checksum_initial=`tar -cf - $dir | md5sum | awk '{print $1}'`
checksum_now=$checksum_initial

# Start nginx
nginx

while true
do
    sleep 3
    checksum_now=`tar -cf - $dir | md5sum | awk '{print $1}'`

    if [ $checksum_initial != $checksum_now ]; then
        echo "[ NGINX ] A configuration file changed. Reloading..."
        nginx -t && nginx -s reload;
    fi

    checksum_initial=$checksum_now
done
Devonte
  • 3,319
  • 5
  • 20
  • 15
  • I like the use of `tar` and `md5sum` to determine if a folder has changed. In order for this to work on macOS, I had to tweak it a little: `checksum_initial=\`tar -cf - $dir | md5\`` – Richard Feb 04 '19 at 22:49
0

If you only need to check for files being created/deleted on top level (not checking subfolders) you might want to use the following.

It uses few ressources hence it can react quickly, I use it to check for a changed file.

#!/bin/bash

file="$1"
shift

tmp=$(mktemp)
trap 'rm "$tmp"' EXIT

while true; do
    while [ ! "$tmp" -ot "$file" ]; do
        sleep 0.5
    done
    eval "$@ &"
    echo $! > "$tmp"
    wait
done
exic
  • 2,220
  • 1
  • 22
  • 29
0

Here's a template to work with, it'll check every 120 seconds for changes in passed directory and notify on creation of directories,files,or names pipes. If you also want to run commands when something is removed then check my other answer on stackoverflow for additional looping examples.

#!/usr/bin/env bash
Var_dir="${1:-/tmp}"
Var_diff_sleep="${2:-120}"
Var_diff_opts="--suppress-common-lines"
Func_parse_diff(){
    _added="$(grep -E '>' <<<"${@}")"
    if [ "${#_added}" != "0" ]; then
        mapfile -t _added_list <<<"${_added//> /}"
        _let _index=0
        until [ "${#_added_list[@]}" = "${_index}" ]; do
            _path_to_check="${Var_dir}/${_added_list[${_index}]}"
            if [ -f "${_path_to_check}" ]; then
                echo "# File: ${_path_to_check}"
            elif [ -d "${_path_to_check}" ]; then
                echo "# Directory: ${_path_to_check}"
            if [ -p "${_path_to_check}" ]; then
                echo "# Pipe: ${_path_to_check}"
            fi
            let _index++
        done
        unset _index
    fi
}
Func_watch_bulk_dir(){
    _current_listing=""
    while [ -d "${Var_dir}" ]; do
        _new_listing="$(ls "${Var_dir}")"
        _diff_listing="$(diff ${Var_dec_diff_opts} <(${Var_echo} "${_current_listing}") <(${Var_echo} "${_new_listing}"))"
        if [ "${_diff_listing}" != "0" ]; then
            Func_parse_diff "${_diff_listing}"
        fi
        _current_listing="${_new_listing}"
        sleep ${Var_diff_sleep}
    done
}

Hint if you replace the echo lines above with eval <some command> for each type of action monitored for you'll be all the closer to automation of actions. And if you wish to see what the above looks like when used within a script then check out the latest script version for the project I've been working on for automation of encryption and decryption via gpg and tar.

Community
  • 1
  • 1
S0AndS0
  • 860
  • 1
  • 7
  • 20
0

Why not using AppleScript

http://www.tuaw.com/2009/03/26/applescript-exploring-the-power-of-folder-actions-part-iii/

on adding folder items to this_folder after receiving added_items
tell application "Finder"
...
Eric Fortis
  • 16,372
  • 6
  • 41
  • 62
  • ok, let me correct myself. This works for receiving files but not responding to changes. I found this out the hard way spending a whole week trying to emulate dropbox. So my point is you cant "watch" a folder completely especially with folder actions. http://stackoverflow.com/questions/6476166/applescript-folder-actions-support-for-changed-altered-updated-files – ThomasReggi Aug 02 '11 at 00:53
0

Radek's answer "sort of" worked for me (OSx) but it slows down my terminal. I made some modifications on it and here's what I think works for me:

#!/bin/bash

daemon() {
    chsum1=""
    targetFolder=path-to-target-folder

    while [[ true ]]
    do
        chsum2=`find ${targetFolder} -type f | xargs stat -f "%m" | md5`
        if [[ $chsum1 != $chsum2 ]] ; then
            # echo the date to indicate we are updating   
            date
            ./do-something.sh
            # tracks the check-sum
            chsum1=$chsum2
        fi
        # sleep for 2 secs
        sleep 2
    done
}

daemon 

The meat is in:

chsum2=`find ${targetFolder} -type f | xargs stat -f "%m" | md5`

which means find all files in ${targetFolder}, pipe that into stat -f "%m" 1-by-1 and then pipe that into md5. stat -f "%m" [filepath] gives you the last modified timestamp.

Do help me improve it. Thanks!