当文件或目录更改时如何运行 shell 脚本?

我希望在特定文件或目录更改时运行 shell 脚本。

我怎么能轻易做到呢?

95319 次浏览

Check out the kernel filesystem monitor daemon

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

Here's a how-to:

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

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

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 .

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

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

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

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.

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 }')"

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
}

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

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.

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

Quick solution for fish shell users who wanna track a single file:

while true
set old_hash $hash
set hash (md5sum file_to_watch)
if [ $hash != $old_hash ]
command_to_execute
end
sleep 1
end

replace md5sum with md5 if on macos.