On file change run...


Problem

I often find myself wanting to re-run some command (say pdflatex) when some file changes.

It’s not a particularly difficult problem, but here’s my take on it.

Solution

#!/usr/bin/ruby

unless ARGV.size == 2
  puts "Usage: #{File.basename($0)} <file> <command>"
  exit 1
end

file = ARGV.shift
command = ARGV.shift

unless test(?f, file)
  puts "Invalid file: #{file}"
  exit 1
end

mt = File.stat(file).mtime

pid = nil

puts "+++ Watching ..."
begin
  loop do
    Process.wait(-1, Process::WNOHANG) rescue nil
    mt_new = File.stat(file).mtime rescue next
    unless mt_new == mt
      puts "+++ File (#{file}) changed ... launching command."
      if pid
        Process.kill(9, pid) if (Process.kill(0, pid) rescue nil)
        pid = nil
      end
      pid = fork do
        Kernel.exec("sh", "-c", command)
      end
      puts "+++ Watching ..."
      mt = mt_new
    end
    sleep 0.5
  end
rescue Interrupt
end

Updated solution from ca. 2014

#!/usr/bin/ruby

if ARGV.size < 2
  puts "Usage: #{File.basename($0)} <file/glob> <command> <params>*"
  exit 1
end

file = ARGV.shift
command = ARGV
refresh = proc { Dir[file] }
files = refresh.call

if files.all? { |x| FileTest.file?(x) }
  puts "Watching: '#{file}' (#{files.size} entries as of now)"
else
  puts "Invalid file/glob: #{file}"
  exit 1
end

mt = nil # mtimes
pid = nil

puts "+++ Watching ..."
begin
  loop do
    Process.wait(-1, Process::WNOHANG) rescue nil
    files = refresh.call unless refresh.nil?
    mt_new = files.map { |x| File.stat(x).mtime rescue nil }
    if mt && mt_new != mt
      puts "+++ File (#{file}) changed ... launching command."
      if pid
        Process.kill(9, pid) if (Process.kill(0, pid) rescue nil)
        pid = nil
      end
      pid = fork do
        Kernel.exec("sh", "-c", *command)
      end
      puts "+++ Watching ..."
    end
    mt = mt_new
    sleep 0.5
  end
rescue Interrupt
end