#!/usr/bin/env bash _VERSION="0.2.0" function help() { I=" " cat << USAGE fsw - run a command when a file is modified - v$_VERSION Usage: ${I}fsw [filter] [dirs...] ${I}command - the specified bash command to eval ${I}filter - an optional filename filter ${I}dirs - the directories to watch (defaults to current directory) Advanced: ${I}Command Variables: ${I}${I}Your commands have runtime access to some variables associated with the ${I}${I}${I}inotifywait event enabling further extensibility: ${I}${I}+ FSW_FILENAME - the name of the file ${I}${I}+ FSW_DIR - the directory containing the file ${I}${I}+ FSW_PATH - the full relative path of the file ${I}${I}+ FSW_FILE_EVENTS - a comma-separated list of the inotify events ${I}Environment: ${I}${I}Some customization options exist using environment variables ${I}${I}${I}due to their uncommon usage. ${I}${I}+ FSW_EVENTS - the list of events to have inotifywait observe References: ${I}+ Filter works with grep -P ${I}+ See inotifywait(1) for FSW_EVENTS options. Examples: ${I}fsw 'make' '\.c$' ${I}fsw 'bash \$filename' '\.bash$' ${I}fsw 'mix test --failed' '.exs?$' lib test ${I}FSW_EVENTS=open,access fsw 'espeak "Intruder Alert!"' \\ ${I}${I}${I}'.*' /etc/secrets \$HOME/.secrets USAGE } dbg() { if [[ -n ${FSW_DEBUG+x} ]]; then echo -e "[debug] fsw: $*" fi } contains_element () { local e match="$1"; shift for e; do [[ "$e" == "$match" ]] && return 0; done return 1 } if [[ $1 = '-h' ]] || [[ $1 = '--help' ]] || [[ -z $1 ]]; then help exit 0 fi FSW_EVENTS="${FSW_EVENTS:-close_write}" dbg "Events: $FSW_EVENTS" SHELL_COMMAND="${1}"; shift dbg "Command: $SHELL_COMMAND" FILTER="${1}"; shift dbg "Filter: $FILTER" dbg "Directory: ${1}" DIRS=("${1:-.}"); shift if [[ -e $FILTER ]]; then # TODO: this is a sad hack/workaround echo "It looks like your filter is an actual file. I'll just watch that for you." DIRS=("${FILTER}") fi while [[ -n $1 ]] && realpath "$1" &> /dev/null; do dbg "Directory: ${1}" DIRS+=("$1"); shift done inotifywait -m -e "${FSW_EVENTS},delete,delete_self" -r "${DIRS[@]}" 2>&1 \ | grep --line-buffered -v ' Beware: since -r was given, this may take a while!' \ | while read -r dir events filename; do if contains_element "$dir" "${DIRS[@]}" && [[ $events =~ "delete" ]]; then echo "One of the watched directories (\"$dir\") was deleted. Exiting..." exit 75 fi if [[ "$dir $events" = "Watches established." ]]; then echo "Ready." dbg "Directory: ${DIRS[*]}" else export FSW_FILENAME="$filename" export FSW_DIR="$dir" export FSW_PATH="$dir$filename" export FSW_FILE_EVENTS="$events" export FSW_EVENT="$events $dir$filename" dbg "Event:\n $(date)\n $FSW_PATH\n $FILTER\n $FSW_EVENT\n $dir $events $filename\n ${SHELL_COMMAND}" dbg "Filtered Event: $(<<< "$FSW_PATH" grep -P "$FILTER")" <<< "$FSW_PATH" grep -P "$FILTER" > /dev/null 2>&1 && eval "${SHELL_COMMAND}" fi done