Source

nosier / nosier / nosier.py

Full commit
import subprocess
import inotifyx
import optparse
import fnmatch
import time
import sys
import os

USAGE = '''\
Usage: %prog [options] command

Monitor directories and files for changes.  On detecting a change run
the specified command.'''

VERSION = '0.10'
VERSION_TEXT = '%%prog version %s' % VERSION

WATCH_EVENTS = inotifyx.IN_CREATE | inotifyx.IN_MODIFY | inotifyx.IN_DELETE | inotifyx.IN_DELETE_SELF | inotifyx.IN_MOVE
BUILTIN_BLACK_LIST = ['.hg', '.git', '.bzr', '.svn', '#*', '.#*', '*.swp']
WIDTH = 80


def iteration_generator():
    i = 0
    while True:
        i += 1
        yield i

iterations = iteration_generator()


def run_command(command, fd, watches, events, ignore_events, white_listed):
    # Run the specified command.
    print '=' * WIDTH
    print 'Iteration: %s' % iterations.next()
    print 'Files    : %s' % ' '.join(set(os.path.join(watches.get(event.wd, ''), event.name or '') for event in events))
    print 'Command  : %s' % ' '.join(command)
    print ''

    subprocess.call(command)

    print ''

    if ignore_events:
        events = inotifyx.get_events(fd, 0)
        while events:
            events = [event for event in events if not event.name or white_listed(event.name)]
            if events:
                print 'Ignoring events for files: %s' % ' '.join(set(os.path.join(watches.get(event.wd, ''), event.name or '') for event in events))
            events = inotifyx.get_events(fd, 0)
        print ''

    print '-' * WIDTH


def main():
    # Process command line options.
    parser = optparse.OptionParser(usage=USAGE, version=VERSION_TEXT)
    parser.add_option('-p', '--path', action='append',
                      help='add a path to monitor for changes, if no paths are specified then the current directory will be monitored')
    parser.add_option('-d', '--delay', type='float', default=0.1,
                      help='how long to wait for additional events after a command run is triggered, defaults to %default second')
    parser.add_option('-i', '--ignore-events', action='store_true', default=False,
                      help='whether to ignore events that occur during the command run, defaults to %default')
    parser.add_option('-w', '--white-list', action='append', default=[], metavar='FILE',
                      help='add a file to the white list, ensure globs are quoted to avoid shell expansion')
    parser.add_option('-b', '--black-list', action='append', default=[], metavar='FILE',
                      help='add a file to the black list, ensure globs are quoted to avoid shell expansion')
    parser.add_option('-l', '--no-default-black-list', action='store_true', default=False,
                      help='''don't add the following to the black list: %s''' % ' '.join(BUILTIN_BLACK_LIST))
    parser.add_option('-r', '--no-initial-run', action='store_true', default=False,
                      help='''don't perform an initial run of the command, instead start monitoring and wait for changes''')

    options, command = parser.parse_args()

    paths = options.path or ['.']
    delay = options.delay
    ignore_events = options.ignore_events
    white_list = options.white_list
    black_list = options.black_list
    if not options.no_default_black_list:
        black_list.extend(BUILTIN_BLACK_LIST)
    no_initial_run = options.no_initial_run

    # Fn that indicates if name is in or out.
    def white_listed(name):
        for pattern in white_list:
            if fnmatch.fnmatch(name, pattern):
                return True
        for pattern in black_list:
            if fnmatch.fnmatch(name, pattern):
                return False
        return True

    # Init inotify.
    fd = inotifyx.init()

    # Watch specified paths.
    print 'Setting up watches on paths'

    watches = {}
    watches.update((inotifyx.add_watch(fd, path, WATCH_EVENTS), path)
                   for path in paths)

    # Watch sub dirs of specified paths.  Ensure we modify dirs in
    # place so that os.walk only traverses white listed dirs.
    for path in paths:
        for root, dirs, files in os.walk(path):
            dirs[:] = [dir for dir in dirs if white_listed(dir)]
            watches.update((inotifyx.add_watch(fd, os.path.join(root, dir), WATCH_EVENTS), os.path.join(root, dir))
                           for dir in dirs)

    print 'Watching %d paths' % len(watches)

    # Initial command run.
    if not no_initial_run:
        run_command(command, fd, watches, [], ignore_events, white_listed)

    # Monitor and run the specified command until keyboard interrupt.
    while True:
        # Block until events arrive.
        events = inotifyx.get_events(fd)

        # Continue collecting events for the delay period.  This
        # allows events that occur close to the trigger event to be
        # collected now rather than causing another run immediately
        # after this run.
        end = time.time() + delay
        while time.time() < end:
            events.extend(inotifyx.get_events(fd, 0))

        # Filter to events that have no name (self destruct watch
        # events) and to events that have names that are considered
        # white listed.
        events = [event for event in events if not event.name or white_listed(event.name)]

        # Track watched dirs.
        for e in events:
            if e.mask & inotifyx.IN_ISDIR and e.mask & inotifyx.IN_CREATE:
                watches[inotifyx.add_watch(fd, os.path.join(watches.get(e.wd), e.name), WATCH_EVENTS)] = os.path.join(watches.get(e.wd), e.name)
            elif e.mask & inotifyx.IN_DELETE_SELF:
                watches.pop(e.wd, None)

        # Do command run provided we have events after white listing.
        if events:
            run_command(command, fd, watches, events, ignore_events, white_listed)


if __name__ == '__main__':
    main()