PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/bin/inotifyexec.py

https://bitbucket.org/mikebentley15/configurations
Python | 130 lines | 110 code | 7 blank | 13 comment | 5 complexity | ccc1456ff9ca6cb8cf602d5693c213b3 MD5 | raw file
  1. #!/usr/bin/env python3
  2. 'Use inotify-tools to watch a directory or files and execute a command'
  3. __author__ = 'Michael Bentley'
  4. import subprocess as subp
  5. import sys
  6. def popen_reader(*args, stdout=subp.PIPE, **kwargs):
  7. '''Creates a pipe (on stdout by default) and returns a line generator
  8. Example:
  9. for line in popen_reader(['ls', '-ltr'], text=True):
  10. line = line[:-1] # remove newline
  11. pieces = line.split() # split into columns
  12. print(pieces[0], pieces[4], pieces[-1]) # subset of columns
  13. Defaults the stdout=subprocess.PIPE instead of stdout=None. You can change
  14. this and/or set stderr=subprocess.PIPE to include that in the line output
  15. to be received.
  16. This is different from subprocess.check_output() in that you can process
  17. the output while the process is still running. So this function can be
  18. used for long-running programs or even programs that may never end. It's a
  19. good way to add asynchronous execution to your python program.
  20. '''
  21. with subp.Popen(*args, stdout=stdout, **kwargs) as proc:
  22. for line in proc.stdout:
  23. yield line
  24. def inotifywait(args):
  25. '''Used in a for statement like so
  26. files = glob.glob('*.cpp')
  27. for file, types in inotifywait(['--monitor'] + files):
  28. # do something
  29. The args will be given to the `inotifywait` command-line tool from
  30. the inotify-tools, so call `inotifywait --help` to see what the args
  31. are.
  32. If you want to use this in a loop like above for multiple file events, use
  33. the `-m` or `--monitor` flag so that it doesn't exit on the first event.
  34. This command will not work appropriately if you use the `-d` or `--daemon`
  35. flag since this command parses the command-line args.
  36. This automatically adds the --recursive and --quiet flags, so you can pass
  37. files or directories as args. If you passed a filename instead of a
  38. directory, the filename returned will be None when the event happens.
  39. Returns a tuple of two values:
  40. - fname (str): filename (or directory) that triggered the event
  41. - types list(str): list of types of events that triggered
  42. For example: ['OPEN', 'ISDIR']
  43. '''
  44. command = [
  45. 'inotifywait',
  46. '--recursive', # allow for directories
  47. '--format', '%w%f %e', # specific format of output
  48. '--quiet', # suppress info at the start
  49. ]
  50. command.extend(args)
  51. for line in popen_reader(command, text=True):
  52. split = line.rsplit(maxsplit=1)
  53. types = split[-1].split(',')
  54. fname = split[0] if len(split) == 2 else None
  55. yield (fname, types)
  56. def inotifyexec(args, command, filter=None, **kwargs):
  57. '''Pass args to inotifywait, and run the given command subprocess
  58. The command will be given to subprocess.check_call() along with any extra
  59. forwarded keyword arguments.
  60. You may provide a filter function that takes two arguments, the filepath
  61. and this list of event types. Note that the filepath will be None if it
  62. was given in args not inside of a directory (see inotifywait()). By
  63. default, the command will be called regardless of the type of triggered
  64. event or the directory or file where the event took place.
  65. '''
  66. if filter is None:
  67. filter = lambda fname, types: True # dummy function
  68. for fname, types in inotifywait(args):
  69. if filter(fname, types):
  70. subp.check_call(command, **kwargs)
  71. def main(arguments=sys.argv[1:]):
  72. if '-h' in arguments or '--help' in arguments:
  73. oldhelp = subp.run(
  74. ['inotifywait', '--help'],
  75. stdout=subp.PIPE,
  76. check=False,
  77. text=True,
  78. )
  79. print('inotifyexec: a wrapper around inotifywait to run a command')
  80. print('Usage: inotifyexec <inotifywait-args> -- <inotifyexec-args>')
  81. print('inotifyexec-args:')
  82. print(' command Any arguments not given to flags are interpreted')
  83. print(' as arguments for a bash command')
  84. print()
  85. print(oldhelp.stdout)
  86. return 0
  87. if '--' not in arguments:
  88. print('Error: you must use "--" to indicate the start of the'
  89. ' inotifyexec arguments', file=sys.stderr)
  90. return 1
  91. idx = arguments.index('--')
  92. args = arguments[:idx]
  93. command = arguments[idx+1:]
  94. if not command:
  95. print('Error: you must specify a command to run on an event',
  96. file=sys.stderr)
  97. return 1
  98. def filter(fname, types):
  99. print(f'{fname} changed: (types = {types})')
  100. return True
  101. return inotifyexec(args, command, filter=filter)
  102. if __name__ == '__main__':
  103. try:
  104. sys.exit(main())
  105. except KeyboardInterrupt:
  106. sys.exit(0)