├── .editorconfig ├── .github └── workflows │ ├── linting.yml │ ├── test-linux.yml │ ├── test-macos.yml │ └── test-windows.yml ├── LICENSE ├── Makefile ├── README.md ├── bin └── watcherd └── tests └── 01.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Default for all files 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # Custom files 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [.sh}] 17 | indent_style = tab 18 | indent_size = 4 19 | 20 | [Makefile] 21 | indent_style = tab 22 | indent_size = 4 23 | 24 | [*.{yml,yaml}] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [*.md] 29 | indent_style = space 30 | trim_trailing_whitespace = false 31 | indent_size = 2 32 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### 4 | ### Lints all generic and json files in the whole git repository 5 | ### 6 | 7 | name: linting 8 | on: 9 | pull_request: 10 | push: 11 | branches: 12 | - master 13 | tags: 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: False 20 | matrix: 21 | target: 22 | - Linting 23 | name: "[ ${{ matrix.target }} ]" 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@master 27 | 28 | - name: Lint files 29 | run: | 30 | make _lint-files 31 | 32 | - name: Shellcheck 33 | run: | 34 | make _lint-shell 35 | -------------------------------------------------------------------------------- /.github/workflows/test-linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-linux 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: False 15 | 16 | name: "[test] [linux]" 17 | steps: 18 | # ------------------------------------------------------------ 19 | # Setup 20 | # ------------------------------------------------------------ 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | # ------------------------------------------------------------ 25 | # Tests: Behaviour 26 | # ------------------------------------------------------------ 27 | - name: test 28 | shell: bash 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/test-macos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-macos 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | 10 | jobs: 11 | test: 12 | runs-on: macos-latest 13 | strategy: 14 | fail-fast: False 15 | 16 | name: "[test] [macos]" 17 | steps: 18 | # ------------------------------------------------------------ 19 | # Setup 20 | # ------------------------------------------------------------ 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | # ------------------------------------------------------------ 25 | # Tests: Behaviour 26 | # ------------------------------------------------------------ 27 | - name: test 28 | shell: bash 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/test-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-windows 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | 10 | jobs: 11 | test: 12 | runs-on: windows-latest 13 | strategy: 14 | fail-fast: False 15 | 16 | name: "[test] [windows]" 17 | steps: 18 | # ------------------------------------------------------------ 19 | # Setup 20 | # ------------------------------------------------------------ 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | # ------------------------------------------------------------ 25 | # Tests: Behaviour 26 | # ------------------------------------------------------------ 27 | - name: test 28 | shell: bash 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 cytopia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifneq (,) 2 | .error This Makefile requires GNU Make. 3 | endif 4 | 5 | # ------------------------------------------------------------------------------------------------- 6 | # Default configuration 7 | # ------------------------------------------------------------------------------------------------- 8 | .PHONY: help lint test 9 | 10 | FL_VERSION = 0.3 11 | FL_IGNORES = .git/,.github/ 12 | 13 | 14 | # ------------------------------------------------------------------------------------------------- 15 | # Default Target 16 | # ------------------------------------------------------------------------------------------------- 17 | help: 18 | @echo "lint Lint repository" 19 | @echo "test Run integration tests" 20 | 21 | 22 | # ------------------------------------------------------------------------------------------------- 23 | # Lint Targets 24 | # ------------------------------------------------------------------------------------------------- 25 | lint: _lint-files 26 | lint: _lint-shell 27 | 28 | 29 | .PHONY: _lint-files 30 | _lint-files: 31 | @echo "# -------------------------------------------------------------------- #" 32 | @echo "# Lint files" 33 | @echo "# -------------------------------------------------------------------- #" 34 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-cr --text --ignore '$(FL_IGNORES)' --path . 35 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-crlf --text --ignore '$(FL_IGNORES)' --path . 36 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-trailing-single-newline --text --ignore '$(FL_IGNORES)' --path . 37 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-trailing-space --text --ignore '$(FL_IGNORES)' --path . 38 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-utf8 --text --ignore '$(FL_IGNORES)' --path . 39 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data cytopia/file-lint:$(FL_VERSION) file-utf8-bom --text --ignore '$(FL_IGNORES)' --path . 40 | 41 | .PHONY: _lint-shell 42 | _lint-shell: 43 | @echo "# -------------------------------------------------------------------- #" 44 | @echo "# Shellcheck" 45 | @echo "# -------------------------------------------------------------------- #" 46 | @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/mnt -w /mnt koalaman/shellcheck:stable bin/watcherd 47 | 48 | 49 | # ------------------------------------------------------------------------------------------------- 50 | # Test Targets 51 | # ------------------------------------------------------------------------------------------------- 52 | test: 53 | ./tests/01.sh 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watcherd 2 | 3 | ![tag](https://img.shields.io/github/v/tag/devilbox/watcherd.svg?colorB=orange&sort=semver) 4 | [![linting](https://github.com/devilbox/watcherd/workflows/linting/badge.svg)](https://github.com/devilbox/watcherd/actions/workflows/linting.yml) 5 | [![test-linux](https://github.com/devilbox/watcherd/workflows/test-linux/badge.svg)](https://github.com/devilbox/watcherd/actions/workflows/test-linux.yml) 6 | [![test-macos](https://github.com/devilbox/watcherd/workflows/test-macos/badge.svg)](https://github.com/devilbox/watcherd/actions/workflows/test-macos.yml) 7 | [![test-windows](https://github.com/devilbox/watcherd/workflows/test-windows/badge.svg)](https://github.com/devilbox/watcherd/actions/workflows/test-windows.yml) 8 | [![License](https://img.shields.io/badge/license-MIT-%233DA639.svg)](https://opensource.org/licenses/MIT) 9 | 10 | 11 | **[watcherd](https://github.com/devilbox/watcherd/blob/master/bin/watcherd)** will look for directory changes (added and deleted directories) under the specified path (`-p`) and will execute specified commands or shell scripts (`-a`, `-d`) depending on the event. 12 | Once all events have happened during one round (`-i`), a trigger command can be executed (`-t`). 13 | Note, the trigger command will only be execute when at least one add or delete command has succeeded with exit code 0. 14 | 15 | --- 16 | 17 | If you need the same functionality to monitor changes of listening ports, check out **[watcherp](https://github.com/devilbox/watcherp)**. 18 | 19 | --- 20 | 21 | ### Modes 22 | 23 | **[watcherd](https://github.com/devilbox/watcherd/blob/master/bin/watcherd)** can either use the native [inotifywait](https://linux.die.net/man/1/inotifywait) implementation or if this is not available on your system use a custom bash implementation. The default is to use bash. 24 | 25 | ### Placeholders 26 | 27 | There are two placeholders available that make it easier to use custom commands/scripts for the add (`-a`) or delete (`-d`) action.: 28 | 29 | * `%p` Full path to the directory that was added or deletd 30 | * `%n` Name of the directory that was added or deleted 31 | 32 | You can specify the placeholders as many times as you want. See the following example section for usage. 33 | 34 | ### Examples 35 | 36 | By using **[vhost-gen](https://github.com/devilbox/vhost-gen)** (which is capable of creating Nginx or Apache vhost config files for normal vhosts or reverse proxies), the following will be able to create new nginx vhosts on-the-fly, simply by adding or deleting folders in your main www directory. The trigger command will simply force nginx to reload its configuration after directory changes occured. 37 | 38 | ```shell 39 | # %n will be replaced by watcherd with the new directory name 40 | # %p will be replaced by watcherd with the new directory path 41 | watcherd -v \ 42 | -p /shared/httpd \ 43 | -a "vhost-gen -p %p -n %n -s" \ 44 | -d "rm /etc/nginx/conf.d/%n.conf" \ 45 | -t "nginx -s reload" 46 | ``` 47 | 48 | ### Usage 49 | 50 | ```bash 51 | Usage: watcherd -p -a -d [-t -w -i -v -c] 52 | watcherd --help 53 | watcherd --version 54 | 55 | watcherd will look for directory changes (added and deleted directories) under 56 | the specified path (-p) and will execute specified commands or shell scripts 57 | (-a, -d) depending on the event. Once all events have happened during one round 58 | (-i), a trigger command can be executed (-t). Note, the trigger command will 59 | only be execute when at least one add or delete command has succeeded with exit 60 | code 0. 61 | 62 | Required arguments: 63 | -p Path to directoy to watch for changes. 64 | -a Command to execute when a directory was added. 65 | You can also append the following placeholders to your command string: 66 | %p The full path of the directory that changed (added, deleted). 67 | %n The name of the directory that changed (added, deleted). 68 | Example: -a "script.sh -f %p -c %n -a %p" 69 | -d Command to execute when a directory was deletd. 70 | You can also append the following placeholders to your command string: 71 | %p The full path of the directory that changed (added, deleted). 72 | %n The name of the directory that changed (added, deleted). 73 | Example: -d "script.sh -f %p -c %n -a %p" 74 | 75 | Optional arguments: 76 | -e Exclude regex for directories to ignore. 77 | E.g.: -e '\.*' to ignore dot directories. 78 | -t Command to execute after all directories have been added or 79 | deleted during one round. 80 | No argument will be appended. 81 | -w The directory watcher to use. Valid values are: 82 | 'inotify': Uses inotifywait to watch for directory changes. 83 | 'bash': Uses a bash loop to watch for directory changes. 84 | The default is to use 'bash' as the watcher. 85 | -i When using the bash watcher, specify the interval in seconds 86 | for how often to look for directory changes. 87 | -v Verbose output. 88 | -c Colorized log output. 89 | 90 | Misc arguments: 91 | --help Show this help screen. 92 | --version Show version information. 93 | ``` 94 | 95 | ### License 96 | 97 | **[MIT License](LICENSE)** 98 | 99 | Copyright (c) 2017 [cytopia](https://github.com/cytopia) 100 | -------------------------------------------------------------------------------- /bin/watcherd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2017 cytopia 6 | # 7 | 8 | ############################################################ 9 | # Settings 10 | ############################################################ 11 | 12 | # Be strict 13 | set -e 14 | set -u 15 | set -o pipefail 16 | 17 | # Required to loop over newlines instead of spaces 18 | IFS=$'\n' 19 | 20 | 21 | 22 | ############################################################ 23 | # Variables 24 | ############################################################ 25 | 26 | # Versioning 27 | MY_NAME="watcherd" 28 | MY_DATE="2023-03-01" 29 | MY_URL="https://github.com/devilbox/watcherd" 30 | MY_AUTHOR="cytopia " 31 | MY_GPGKEY="0xA02C56F0" 32 | MY_VERSION="1.1.0" 33 | MY_LICENSE="MIT" 34 | 35 | # Default settings 36 | COLORIZED=0 37 | INTERVAL=1 38 | VERBOSE=0 39 | WATCHER="bash" 40 | WATCH_DIR= 41 | EXCLUDE= 42 | CMD_ADD= 43 | CMD_DEL= 44 | CMD_TRIGGER= 45 | 46 | # Do not create subshell when comparing directories 47 | # This is useful when running this script by supervisord 48 | # as it would otherwise see too many process spawns. 49 | WITHOUT_SUBSHELL=1 50 | 51 | 52 | 53 | ############################################################ 54 | # Functions 55 | ############################################################ 56 | 57 | log() { 58 | local type="${1}" # err, warn, info, ok 59 | local message="${2}" # message to log 60 | 61 | # https://unix.stackexchange.com/questions/124407/what-color-codes-can-i-use-in-my-bash-ps1-prompt 62 | if [ "${COLORIZED:-}" = "1" ]; then 63 | local clr_green="\033[0;32m" 64 | local clr_yellow="\033[0;33m" 65 | local clr_red="\033[0;31m" 66 | local clr_rst="\033[0m" 67 | else 68 | local clr_green= 69 | local clr_yellow= 70 | local clr_red= 71 | local clr_rst= 72 | fi 73 | 74 | if [ "${type}" = "err" ]; then 75 | printf "%s: ${clr_red}[ERR] %s${clr_rst}\n" "${MY_NAME}" "${message}" 1>&2 # stdout -> stderr 76 | fi 77 | if [ "${type}" = "warn" ]; then 78 | printf "%s: ${clr_yellow}[WARN] %s${clr_rst}\n" "${MY_NAME}" "${message}" 1>&2 # stdout -> stderr 79 | fi 80 | if [ "${VERBOSE:-}" = "1" ]; then 81 | if [ "${type}" = "info" ]; then 82 | printf "%s: [INFO] %s\n" "${MY_NAME}" "${message}" 83 | fi 84 | if [ "${type}" = "ok" ]; then 85 | printf "%s: ${clr_green}[OK] %s${clr_rst}\n" "${MY_NAME}" "${message}" 86 | fi 87 | fi 88 | } 89 | 90 | function print_help() { 91 | printf "Usage: %s %s\\n" "${MY_NAME}" "-p -a -d [-t -w -i -v -c]" 92 | printf " %s %s\\n" "${MY_NAME}" "--help" 93 | printf " %s %s\\n" "${MY_NAME}" "--version" 94 | printf "\\n" 95 | printf "%s\\n" "${MY_NAME} will look for directory changes (added and deleted directories) under the specified" 96 | printf "path (-p) and will execute specified commands or shell scripts (-a, -d) depending on the event.\\n" 97 | printf "Once all events have happened during one round (-i), a trigger command can be executed (-t).\\n" 98 | printf "Note, the trigger command will only be execute when at least one add or delete command has succeeded with exit code 0." 99 | printf "\\n" 100 | printf "\\nRequired arguments:\\n" 101 | printf " -p %s\\n" "Path to directoy to watch for changes." 102 | printf " -a %s\\n" "Command to execute when a directory was added." 103 | printf " %s\\n" "You can also append the following placeholders to your command string:" 104 | printf " %s\\n" "%p The full path of the directory that changed (added, deleted)." 105 | printf " %s\\n" "%n The name of the directory that changed (added, deleted)." 106 | printf " %s\\n" "Example: -a \"script.sh -f %p -c %n -a %p\"" 107 | printf " -d %s\\n" "Command to execute when a directory was deletd." 108 | printf " %s\\n" "You can also append the following placeholders to your command string:" 109 | printf " %s\\n" "%p The full path of the directory that changed (added, deleted)." 110 | printf " %s\\n" "%n The name of the directory that changed (added, deleted)." 111 | printf " %s\\n" "Example: -d \"script.sh -f %p -c %n -a %p\"" 112 | printf "\\nOptional arguments:\\n" 113 | printf " -e %s\\n" "Exclude regex for directories to ignore." 114 | printf " %s\\n" "E.g.: -e '\\.*' to ignore dot directories." 115 | printf " -t %s\\n" "Command to execute after all directories have been added or deleted during one round." 116 | printf " %s\\n" "No argument will be appended." 117 | printf " -w %s\\n" "The directory watcher to use. Valid values are:" 118 | printf " %s\\n" "'inotify': Uses inotifywait to watch for directory changes." 119 | printf " %s\\n" "'bash': Uses a bash loop to watch for directory changes." 120 | printf " %s\\n" "The default is to use 'bash' as the watcher." 121 | printf " -i %s\\n" "When using the bash watcher, specify the interval in seconds for how often" 122 | printf " %s\\n" "to look for directory changes." 123 | printf " -v %s\\n" "Verbose output." 124 | printf " -c %s\\n" "Colorized log output." 125 | printf "\\nMisc arguments:\\n" 126 | printf " --help %s\\n" "Show this help screen." 127 | printf " --version %s\\n" "Show version information." 128 | } 129 | 130 | function print_version() { 131 | printf "Name: %s\\n" "${MY_NAME}" 132 | printf "Version: %s (%s)\\n" "${MY_VERSION}" "${MY_DATE}" 133 | printf "Author: %s (%s)\\n" "${MY_AUTHOR}" "${MY_GPGKEY}" 134 | printf "License: %s\\n" "${MY_LICENSE}" 135 | printf "URL: %s\\n" "${MY_URL}" 136 | } 137 | 138 | function get_subdirs() { 139 | local path="${1}" 140 | # shellcheck disable=SC2016 141 | #(find "${path}" -type d -print0 || true) \ 142 | # | xargs -0 -n1 sh -c 'if [ -d "${1}" ]; then echo "${1}"; fi' -- \ 143 | # | grep -Ev "^${path}\$" \ 144 | # | grep -Ev "^${path}/.+/" \ 145 | # | sort 146 | path="${path%/}/" 147 | # shellcheck disable=SC2012 148 | cd "${path}" && ls -1 -a . \ 149 | | tr '\r\n' '\000' \ 150 | | tr '\n' '\000' \ 151 | | tr '\r' '\000' \ 152 | | xargs \ 153 | -0 \ 154 | -P"$(getconf _NPROCESSORS_ONLN)" \ 155 | -n1 \ 156 | sh -c " 157 | if [ -d \"\${1}\" ] && [ \"\${1}\" != \".\" ] && [ \"\${1}\" != \"..\" ]; then 158 | if [ -n \"${EXCLUDE}\" ]; then 159 | if ! echo \"\${1}\" | grep -E '${EXCLUDE}' >/dev/null; then 160 | echo \"${path}\${1}\"; 161 | fi 162 | else 163 | echo \"${path}\${1}\"; 164 | fi; 165 | fi" -- \ 166 | | sort 167 | } 168 | 169 | function action() { 170 | local directory="${1}" # Directory to work on 171 | local action="${2}" # Add/Del command to execute 172 | local info="${3}" # Output text (ADD or DEL) for verbose mode 173 | local verbose="${4}" # Verbose? 174 | local name 175 | name="$( basename "${directory}" )" 176 | 177 | # Fill with placeholder values 178 | action="${action//%p/${directory}}" 179 | action="${action//%n/${name}}" 180 | 181 | if eval "${action}"; then 182 | log "ok" "${info} succeeded: ${directory}" 183 | return 0 184 | else 185 | log "err" "${info} failed: ${action}" 186 | log "err" "${info} failed: ${directory}" 187 | return 1 188 | fi 189 | } 190 | 191 | function trigger() { 192 | local action="${1}" # Trigger command to run 193 | local changes="${2}" # Only run trigger when changes == 1 194 | local verbose="${3}" # Verbose? 195 | 196 | # Only run trigger when command has been specified (not empty) 197 | if [ -n "${action}" ]; then 198 | if [ "${changes}" -eq "1" ]; then 199 | if eval "${action}"; then 200 | if [ "${verbose}" -gt "0" ]; then 201 | log "ok" "TRG succeeded: ${action}" 202 | fi 203 | return 0 204 | else 205 | log "err" "TRG failed: ${action}" 206 | # Also return 0 here in order to not abort the loop 207 | return 0 208 | fi 209 | fi 210 | fi 211 | } 212 | 213 | 214 | 215 | ############################################################ 216 | # Read command line arguments 217 | ############################################################ 218 | 219 | while [ $# -gt 0 ]; do 220 | case "${1}" in 221 | -p) 222 | shift 223 | if [ -z "${1:-}" ]; then 224 | >&2 echo "${MY_NAME}: -p requires an argument." 225 | exit 1 226 | fi 227 | if [ ! -d "${1}" ]; then 228 | >&2 echo "${MY_NAME}: Specified directory with -p does not exist: '${1}'." 229 | exit 1 230 | fi 231 | WATCH_DIR="${1}" 232 | ;; 233 | -a) 234 | shift 235 | if [ -z "${1:-}" ]; then 236 | >&2 echo "${MY_NAME}: -a requires an argument." 237 | exit 1 238 | fi 239 | if [ "${1:0:1}" = "-" ]; then 240 | >&2 echo "${MY_NAME}: Specified add command cannot start with '-': '${1}'." 241 | exit 1 242 | fi 243 | CMD_ADD="${1}" 244 | ;; 245 | -d) 246 | shift 247 | if [ -z "${1:-}" ]; then 248 | >&2 echo "${MY_NAME}: -d requires an argument." 249 | exit 1 250 | fi 251 | if [ "${1:0:1}" = "-" ]; then 252 | >&2 echo "${MY_NAME}: Specified del command cannot start with '-': '${1}'." 253 | exit 1 254 | fi 255 | CMD_DEL="${1}" 256 | ;; 257 | -e) 258 | shift 259 | if [ -z "${1:-}" ]; then 260 | >&2 echo "${MY_NAME}: -e requires an argument." 261 | exit 1 262 | fi 263 | EXCLUDE="${1}" 264 | ;; 265 | -t) 266 | shift 267 | if [ -z "${1:-}" ]; then 268 | >&2 echo "${MY_NAME}: -t requires an argument." 269 | exit 1 270 | fi 271 | if [ "${1:0:1}" = "-" ]; then 272 | >&2 echo "${MY_NAME}: Specified trigger command cannot start with '-': '${1}'." 273 | exit 1 274 | fi 275 | CMD_TRIGGER="${1}" 276 | ;; 277 | -w) 278 | shift 279 | if [ -z "${1:-}" ]; then 280 | >&2 echo "${MY_NAME}: -w requires an argument." 281 | exit 1 282 | fi 283 | if [ "${1}" != "bash" ] && [ "${1}" != "inotify" ]; then 284 | >&2 echo "${MY_NAME}: Specified watcher with -w must either be 'bash; or 'inotify': '${1}'." 285 | exit 286 | fi 287 | if [ "${1}" = "inotify" ]; then 288 | if ! command -v inotifywait >/dev/null 2>&1; then 289 | >&2 echo "${MY_NAME}: Specified watcher 'inotify' requires 'inotifywait' binary. Not found." 290 | exit 291 | fi 292 | fi 293 | WATCHER="${1}" 294 | ;; 295 | -i) 296 | shift 297 | if [ -z "${1:-}" ]; then 298 | >&2 echo "${MY_NAME}: -i requires an argument." 299 | exit 1 300 | fi 301 | if ! echo "${1}" | grep -Eq '^[1-9][0-9]*$'; then 302 | >&2 echo "${MY_NAME}: Specified interval with -i is not a valid integer > 0: '${1}'." 303 | exit 1 304 | fi 305 | INTERVAL="${1}" 306 | ;; 307 | -v) 308 | VERBOSE="1" 309 | ;; 310 | -c) 311 | COLORIZED="1" 312 | ;; 313 | --help) 314 | print_help 315 | exit 0 316 | ;; 317 | --version) 318 | print_version 319 | exit 0 320 | ;; 321 | *) 322 | echo "${MY_NAME}: Invalid argument: ${1}. Type --help for available options." 323 | exit 1 324 | ;; 325 | esac 326 | shift 327 | done 328 | 329 | # Make sure required arguments are set 330 | if [ -z "${WATCH_DIR}" ]; then 331 | >&2 echo "${MY_NAME}: Error: -p is required. Type --help for more information." 332 | exit 1 333 | fi 334 | if [ -z "${CMD_ADD}" ]; then 335 | >&2 echo "${MY_NAME}: Error: -a is required. Type --help for more information." 336 | exit 1 337 | fi 338 | if [ -z "${CMD_DEL}" ]; then 339 | >&2 echo "${MY_NAME}: Error: -d is required. Type --help for more information." 340 | exit 1 341 | fi 342 | 343 | 344 | 345 | ############################################################ 346 | # Main entrypoint 347 | ############################################################ 348 | 349 | # Log startup 350 | log "info" "Starting daemon: $( date '+%Y-%m-%d %H:%M:%S' )" 351 | 352 | CHANGES=0 353 | ALL_DIRS="$( get_subdirs "${WATCH_DIR}" )" 354 | 355 | if [ "${WITHOUT_SUBSHELL}" -eq "1" ]; then 356 | LFT_FILE="$( mktemp )" 357 | RGT_FILE="$( mktemp )" 358 | fi 359 | 360 | # Initial add 361 | for d in ${ALL_DIRS}; do 362 | # Only CHANGE if adding was successful 363 | if action "${d}" "${CMD_ADD}" "ADD" "${VERBOSE}"; then 364 | CHANGES=1 365 | fi 366 | done 367 | trigger "${CMD_TRIGGER}" "${CHANGES}" "${VERBOSE}" 368 | CHANGES=0 369 | 370 | 371 | ### 372 | ### Endless loop over changes 373 | ### 374 | 375 | # Use native inotify 376 | if [ "${WATCHER}" = "inotify" ]; then 377 | log "info" "Using native inotify to watch for changes." 378 | if [ -n "${EXCLUDE}" ]; then 379 | inotifywait \ 380 | --monitor \ 381 | --exclude "${EXCLUDE}" \ 382 | --event create \ 383 | --event modify \ 384 | --event delete \ 385 | --event move \ 386 | --format '%e/\\%w%f' \ 387 | "${WATCH_DIR}" | while read -r output; do 388 | d="${output##*\\}" 389 | if [[ "${output}" =~ ^(CREATE|MOVED_TO),ISDIR/\\ ]]; then 390 | if action "${d}" "${CMD_ADD}" "ADD" "${VERBOSE}"; then 391 | trigger "${CMD_TRIGGER}" "1" "${VERBOSE}" 392 | fi 393 | elif [[ "${output}" =~ ^(DELETE|MOVED_FROM),ISDIR/\\ ]]; then 394 | if action "${d}" "${CMD_DEL}" "DEL" "${VERBOSE}"; then 395 | trigger "${CMD_TRIGGER}" "1" "${VERBOSE}" 396 | fi 397 | fi 398 | done 399 | else 400 | inotifywait \ 401 | --monitor \ 402 | --event create \ 403 | --event modify \ 404 | --event delete \ 405 | --event move \ 406 | --format '%e/\\%w%f' \ 407 | "${WATCH_DIR}" | while read -r output; do 408 | d="${output##*\\}" 409 | if [[ "${output}" =~ ^(CREATE|MOVED_TO),ISDIR/\\ ]]; then 410 | if action "${d}" "${CMD_ADD}" "ADD" "${VERBOSE}"; then 411 | trigger "${CMD_TRIGGER}" "1" "${VERBOSE}" 412 | fi 413 | elif [[ "${output}" =~ ^(DELETE|MOVED_FROM),ISDIR/\\ ]]; then 414 | if action "${d}" "${CMD_DEL}" "DEL" "${VERBOSE}"; then 415 | trigger "${CMD_TRIGGER}" "1" "${VERBOSE}" 416 | fi 417 | fi 418 | done 419 | fi 420 | # Use custom inotify 421 | else 422 | log "info" "Using bash loop to watch for changes." 423 | while true; do 424 | # Get all directories 425 | NEW_DIRS="$( get_subdirs "${WATCH_DIR}" )" 426 | 427 | # Compare against previously read directories 428 | if [ "${WITHOUT_SUBSHELL}" -eq "1" ]; then 429 | echo "${ALL_DIRS}" > "${LFT_FILE}" 430 | echo "${NEW_DIRS}" > "${RGT_FILE}" 431 | ADD_DIRS="$( comm -13 "${LFT_FILE}" "${RGT_FILE}" )" 432 | DEL_DIRS="$( comm -23 "${LFT_FILE}" "${RGT_FILE}" )" 433 | else 434 | ADD_DIRS="$( comm -13 <(echo "${ALL_DIRS}") <(echo "${NEW_DIRS}") )" 435 | DEL_DIRS="$( comm -23 <(echo "${ALL_DIRS}") <(echo "${NEW_DIRS}") )" 436 | fi 437 | 438 | # Run ADD command 439 | for d in $ADD_DIRS; do 440 | if action "${d}" "${CMD_ADD}" "ADD" "${VERBOSE}"; then 441 | CHANGES=1 442 | fi 443 | done 444 | 445 | # Run DEL command 446 | for d in $DEL_DIRS; do 447 | if action "${d}" "${CMD_DEL}" "DEL" "${VERBOSE}"; then 448 | CHANGES=1 449 | fi 450 | done 451 | 452 | # Trigger if changes are present 453 | trigger "${CMD_TRIGGER}" "${CHANGES}" "${VERBOSE}" 454 | 455 | # Reset changes 456 | CHANGES=0 457 | 458 | # Update index to currently existing directories 459 | ALL_DIRS="${NEW_DIRS}" 460 | 461 | # Wait before restarting loop 462 | sleep "${INTERVAL}" 463 | done 464 | fi 465 | -------------------------------------------------------------------------------- /tests/01.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -u 5 | set -o pipefail 6 | 7 | SCRIPT_PATH="$( cd "$(dirname "$0")" && pwd -P )" 8 | 9 | BIN_PATH="${SCRIPT_PATH}/../bin" 10 | DIR_PATH="${SCRIPT_PATH}/dirs" 11 | 12 | 13 | 14 | cleanup() { 15 | rm -rf "${DIR_PATH}" || true 16 | rm -rf "${SCRIPT_PATH}/01.actual" || true 17 | rm -rf "${SCRIPT_PATH}/01.expected" || true 18 | rm -rf "${SCRIPT_PATH}/01.tmp" || true 19 | } 20 | 21 | 22 | ### 23 | ### 01. Clean and create test dirs 24 | ### 25 | cleanup 26 | mkdir -p "${DIR_PATH}/dir 1" 27 | mkdir -p "${DIR_PATH}/dir 2" 28 | mkdir -p "${DIR_PATH}/dir 3" 29 | mkdir -p "${DIR_PATH}/dir 4" 30 | mkdir -p "${DIR_PATH}/dir 4/subdir" 31 | touch "${DIR_PATH}/file 1" 32 | touch "${DIR_PATH}/file 2" 33 | 34 | 35 | ### 36 | ### 02. Setup expected 37 | ### 38 | { 39 | echo "[OK] ADD succeeded: ./dir 1" 40 | echo "[OK] ADD succeeded: ./dir 2" 41 | echo "[OK] ADD succeeded: ./dir 3" 42 | echo "[OK] ADD succeeded: ./dir 4" 43 | } > "${SCRIPT_PATH}/01.expected" 44 | 45 | 46 | ### 47 | ### 03. Run watcherd 48 | ### 49 | cd "${DIR_PATH}" 50 | "${BIN_PATH}/watcherd" -v -p "." -a "echo 'add: %p'" -d "echo 'del: %p'" > "${SCRIPT_PATH}/01.tmp" & 51 | watch_pid="${!}" 52 | echo "Started watcherd with pid: ${watch_pid}" 53 | echo "Waiting 5 sec." 54 | sleep 5 55 | 56 | cat "${SCRIPT_PATH}/01.tmp" | grep -Eo '\[OK.*' > "${SCRIPT_PATH}/01.actual" 57 | 58 | ### 59 | ### 04 .Compare results and shutdown 60 | ### 61 | echo "Diff results" 62 | if ! diff "${SCRIPT_PATH}/01.actual" "${SCRIPT_PATH}/01.expected"; then 63 | echo "[ERR] Results did not equal" 64 | echo "Killing watcherd" 65 | kill "${watch_pid}" || true 66 | cleanup 67 | exit 1 68 | fi 69 | 70 | echo "[OK] Results equal." 71 | echo "Killing watcherd" 72 | kill "${watch_pid}" || true 73 | cleanup 74 | exit 0 75 | --------------------------------------------------------------------------------