Skip to content
Snippets Groups Projects
onfilechange.sh 7.43 KiB
#!/bin/bash
# ======================================================================
#
#
#  T R I G G E R    C O M M A N D    O N    A    F I L E C H A N G E
#
#
# A Shell script that watches a given fileobject or multiple fileobjects
# (=files or directories)
# If the fileobject changes then a given command will be exxecuted.
# It loops permanently; you need to stop it by Ctrl + C and/ or can
# use it as systemd watcher daemon.
#
# It uses stat for wide compatibility but can enable inotifywatch to
# check a file change by an event.
#
# licence: GNU GPL 3.0
# source: https://git-repo.iml.unibe.ch/iml-open-source/onfilechange
# docs: https://os-docs.iml.unibe.ch/onfilechange/
#
# ----------------------------------------------------------------------
# 2019-10-14  v1.0   <axel.hahn@iml.unibe.ch>  first basic version
# 2019-10-21  v1.03  <axel.hahn@iml.unibe.ch>  use stat as default
# 2022-03-11  v1.04  <axel.hahn@iml.unibe.ch>  shell fixes
# 2022-03-11  v1.05  <axel.hahn@iml.unibe.ch>  fix: behaviur when trigger command fails; shell fixes; update docs
# ======================================================================

# ----------------------------------------------------------------------
# CONFIG
# ----------------------------------------------------------------------

bDebug=0
iSleep=5
sCommand=
sWatchFile=
sMode=

# ---- below are some internal variables 
_version=1.05


# ----------------------------------------------------------------------
# FUNCTIONS
# ----------------------------------------------------------------------

# show help
function showHelp(){
	local _self=$( basename $0 )
cat <<ENDOFHELP
HELP:
        This script checks the change of a given fileobjects and triggers
        a command if it changes

PRAMETERS:
        -c [command]
            command to execute on a file change
        -f [fileobject(s)]
            filenames or directories to watch; separate multiple files with 
			space and put all in quotes
        -h
            show this help
        -i
            force inotifywait command
        -s
	        force stat command (default mode)
        -v
            verbose mode; enable showing debug output
        -w [integer]
            for stat mode: wait time in seconds betweeen each test or on 
			missing file; default: 5 sec

EXAMPLES:
        $_self -f /home/me/touchfile.txt -c "ls -l" 
            watch touchfile.txt and make a file listing on change

        $_self -f "/home/me/touchfile.txt home/me/touchfile2.txt" -c "ls -l" 
            watch touchfile.txt and touchfile2.txt

        $_self -f /home/me/touchfile.txt -s -w 10 -c "echo hello" 
            watch touchfile.txt every 10 sec with stat and show "hello" on a 
			change

ENDOFHELP

}

# write debug output ... if debug is enabled only
#
# global (bool) $bDebug
# param  string  text message to show
function wd(){
	if [ $bDebug -ne 0 ]; then
		echo "[$(date)] DEBUG |" $*
	fi
}

# list watched files
#
# global (string) filename(s) to watch
function listFiles(){
	echo
	echo ">>>>> watched files"
	ls -ld ${sWatchFile} 2>&1
}

# for stat: helper to get current file status
#
# global (string) $sWatchFile
function getFilestatus(){
	for myfile in ${sWatchFile}
	do
		stat -c "%F %n | perms: %A; user %u (%U) group %g (%G) | size: %s byte | last modification %y" "${myfile}" 2>&1
	done
}

# for stat: inititalize file change detection
#
# global (string) $TmpFile    last/ initial file status
# global (string) $sTmpFile2  current file status
function initFilestatus(){
	getFilestatus >"${sTmpFile}"
	wd "$(cat "${sTmpFile}")"
	cp -p "${sTmpFile}" "${sTmpFile2}"
}

# for stat: compare file status and execute command on change
#
# global (string) $TmpFile    last/ initial file status
# global (string) $sTmpFile2  current file status
# global (string) $sCommand   command to execute
function compareFilestatus(){
	getFilestatus >${sTmpFile2}
	wd "$(cat ${sTmpFile2})"
	if diff ${sTmpFile} ${sTmpFile2}; then
		wd "No Change"
	else
		wd "Change detected."
		if execCommand; then
			echo Command was successful. 
		else
			echo rc=$? FAILED.
		fi
		wd "Re-Init File status"
		mv ${sTmpFile2} ${sTmpFile}
		echo
		echo ">>>>> waiting for the next change ..."
	fi
}

# execute a command; called on a file change
#
# global (string) $sCommand  command line to exectute
function execCommand(){
	echo ">>>>> $(date) Executing ${sCommand} ..."
	${sCommand}
}

# ----------------------------------------------------------------------
# MAIN
# ----------------------------------------------------------------------

cat <<ENDOFHEAD
______________________________________________________________________________

  T R I G G E R    C O M M A N D    O N    A    F I L E C H A N G E   
_______________________________________________________________________| v${_version}

ENDOFHEAD

if which stat >/dev/null 2>&1; then
	echo "INFO: stat command detected"
	sMode=stat
else
	echo ERROR: the command stat was not found on your system.
	if ! which inotifywait >/dev/null 2>&1; then
		echo ERROR: the command inotifywait was not found on your system.
		exit 2
	fi
	echo "INFO: enabling inotifywait command"
	sMode=inotifywait
fi

if [ $# -eq 0 ]; then
	showHelp
	exit 0
fi

while getopts ":c: :v :f: :h :i :s :w:" opt
do
	case $opt in
		h)
			showHelp
			exit 0
			;;
		v)
			bDebug=1
			wd "debug is now ${bDebug}"
			;;
		c)
			sCommand=$OPTARG
			wd "command is now ${sCommand}"
			;;
		f)
			sWatchFile=$OPTARG
			wd "watch file is now ${sWatchFile}"
			;;
		i)
			sMode=inotifywait
			wd "forcing mode with inotifywait command"
			;;
		s)
			sMode=stat
			wd "forcing mode with stat command"
			;;
		w)
				typeset -i iSleep=$OPTARG
				wd "sleep $iSleep sec"
				;;
		:)
				echo "ERROR: Option -$opt requires an argument." >&2
				showHelp
				exit 1
        esac
done

cat <<ENDOFINFO

--- summary
checking file [${sWatchFile}]
with command [${sMode}] 
with a sleep time of [${iSleep}] seconds 
and on change I start the command [${sCommand}]

...............................................................................


ENDOFINFO

# ----------------------------------------------------------------------
# CHECKS
# ----------------------------------------------------------------------

wd "--- checks"
if [ -z "${sWatchFile}" ]; then
	echo ERROR: set a check file with param -f
	exit 1
fi
if ! listFiles; then
	echo "INFO: file ${sWatchFile} (or one of them) does not exist yet"
	# echo "ERROR: file ${sWatchFile} (or one of them) does not exist yet"
	# exit 1
fi

if [ -z "${sCommand}" ]; then
	echo ERROR: set a ${sCommand} with param -s
	exit 1
fi
echo

# ----------------------------------------------------------------------
# GO
# ----------------------------------------------------------------------

echo ">>>>> start"
myset=$(echo "${sWatchFile}" | sha1sum | cut -f 1 -d " ")
sTmpFile="/tmp/$(basename $0)-${myset}-last.tmp"
sTmpFile2="/tmp/$(basename $0)-${myset}-current.tmp"

case $sMode in
	"inotifywait")
		while true; do
			if listFiles >/dev/null 2>&1; then
				inotifywait -e attrib -e modify "${sWatchFile}" && execCommand
			else 
				echo "ERROR: inotifywait only can notify if all watched files exist."
				echo "Use parameter -s to use stat for file detection, This mode also allows that a file is deleted."
				exit 2
			fi
		done
		;;

	"stat")
		wd "--- initial read of watched files"
		initFilestatus
		echo waiting for file changes ...

		wd "--- starting loop"
		while true; do
			wd sleep ${iSleep}
			sleep ${iSleep}
			compareFilestatus
		done
		;;
esac

# ----------------------------------------------------------------------