├── README.mkd └── react.py /README.mkd: -------------------------------------------------------------------------------- 1 | # Command line tool for inotify 2 | 3 | ## Dependencies 4 | 5 | * [pyinotify](https://github.com/seb-m/pyinotify) 6 | * [Python 3.x](http://www.python.org/download/releases/3.2/) | [Python 2.x](http://www.python.org/download/releases/2.7.2/) 7 | * [argparse](http://pypi.python.org/pypi/argparse) 8 | 9 | ## Usage 10 | 11 | See "react --help" 12 | 13 | ### Hello World 14 | `react.py /tmp -p '*.txt' 'echo $f'` 15 | 16 | This is a simple test. Execute the above command and create and modify 17 | directories and files in /tmp. For every file ending with .txt its full path is 18 | printed on the console every time this file is created or changed. 19 | 20 | ### Executing test suites 21 | `react.py ../ -p '*.hs' ./runTests` 22 | 23 | The main motivation of this tool was to be able to trigger the execution of a 24 | test suite each time a source file of the project is modified. This speeds up 25 | my vim-based software development a lot. 26 | 27 | ### Setting file ACL entries 28 | `react.py /srv/www/team 'setfacl -m g:team:rwx $f'` 29 | 30 | A major disadvantage of default [file ACLs](http://linux.die.net/man/1/setfacl) 31 | is that it only reacts on newly created files. If you move a file into a 32 | directory with a default ACL entry, this file's ACL entries are not updated. In 33 | practice this means that sooner or later every default-ACL-based control 34 | management will break. With react you can circumvent this problem. 35 | 36 | ## Information 37 | 38 | * Licence: [GPL 2.0](http://www.gnu.org/licenses/gpl-2.0.html) 39 | * Author: Alexander Bernauer (alex@copton.net) 40 | -------------------------------------------------------------------------------- /react.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import os.path 5 | from pyinotify import WatchManager, IN_DELETE, IN_CREATE, IN_CLOSE_WRITE, ProcessEvent, Notifier 6 | import subprocess 7 | import sys 8 | import re 9 | import argparse 10 | import fnmatch 11 | 12 | class PatternAction(argparse.Action): 13 | def __call__(self, parser, namespace, values, option_string=None): 14 | setattr(namespace, self.dest, fnmatch.translate(values)) 15 | 16 | parser = argparse.ArgumentParser(description='Launch a script if specified files change.') 17 | parser.add_argument('directory', help='the directory which is recursively monitored') 18 | 19 | group = parser.add_mutually_exclusive_group() 20 | group.add_argument('-r', '--regex', required=False, default=".*", help='files only trigger the reaction if their name matches this regular expression') 21 | group.add_argument('-p', '--pattern', required=False, dest="regex", action=PatternAction, help='files only trigger the reaction if their name matches this shell pattern') 22 | 23 | parser.add_argument("script", help="the script that is executed upon reaction") 24 | 25 | class Options: 26 | __slots__=["directory", "regex", "script"] 27 | 28 | options = Options() 29 | args = parser.parse_args(namespace=options) 30 | 31 | class Reload (Exception): 32 | pass 33 | 34 | class Process(ProcessEvent): 35 | def __init__(self, options): 36 | self.regex = re.compile(options.regex) 37 | self.script = options.script 38 | 39 | def process_IN_CREATE(self, event): 40 | target = os.path.join(event.path, event.name) 41 | if os.path.isdir(target): 42 | raise Reload() 43 | 44 | def process_IN_DELETE(self, event): 45 | raise Reload() 46 | 47 | def process_IN_CLOSE_WRITE(self, event): 48 | target = os.path.join(event.path, event.name) 49 | if self.regex.match(target): 50 | args = self.script.replace('$f', target).split() 51 | os.system("clear") 52 | sys.stdout.write("executing script: " + " ".join(args) + "\n") 53 | subprocess.call(args) 54 | sys.stdout.write("------------------------\n") 55 | 56 | while True: 57 | wm = WatchManager() 58 | process = Process(options) 59 | notifier = Notifier(wm, process) 60 | mask = IN_DELETE | IN_CREATE | IN_CLOSE_WRITE 61 | wdd = wm.add_watch(options.directory, mask, rec=True) 62 | try: 63 | while True: 64 | notifier.process_events() 65 | if notifier.check_events(): 66 | notifier.read_events() 67 | except Reload: 68 | pass 69 | except KeyboardInterrupt: 70 | notifier.stop() 71 | break 72 | --------------------------------------------------------------------------------