Skip to content

Instantly share code, notes, and snippets.

@michaelvobrien
Created September 29, 2016 22:25
Show Gist options
  • Save michaelvobrien/cbc974a47d3311d8eed6551b4c92ee37 to your computer and use it in GitHub Desktop.
Save michaelvobrien/cbc974a47d3311d8eed6551b4c92ee37 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# (The MIT License)
#
# Copyright (c) 2015 Michael V. O'Brien
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'singleton'
require 'optparse'
require 'ostruct'
begin
require 'listen'
rescue LoadError
STDERR.puts "listen gem required"
exit!
end
module Watchful
VERSION = '0.0.2'
class Command
include Singleton
attr_reader :settings
def initialize
@settings = OpenStruct.new
@settings.path = pwd
@settings.only_list = []
@settings.exclude_list = []
end
def pwd
ENV['PWD']
end
def expand_path(path)
File.expand_path path
end
def validate_path(path)
handle_invalid_path(path) unless File.exists?(path)
end
def handle_invalid_path(path)
STDERR.puts "invalid path '#{path}'"
exit!
end
def run_command
@settings.command_path ||= @settings.path
info "#{@settings.command_path}: #{@settings.command}"
system(@settings.command, chdir: @settings.command_path)
end
def watch
listener = Listen.to(@settings.path) do |modified, added, removed|
run_command
end
@settings.only_list.each do |only|
listener.only(/#{only}/)
end
@settings.exclude_list.each do |exclude|
listener.ignore(/#{exclude}/)
end
listener.start # non-blocking
message = "Watching '#{@settings.path}'"
message = message + ", only: '#{@settings.only}'" if @settings.only
info message
sleep
end
def shut_down
puts "\n"
end
def handle_signals
# ^C
Signal.trap("INT") {
shut_down
exit
}
end
def info(string)
bold = `tput bold`
green = `tput setaf 2`
reset = `tput sgr0`
timestamp = Time.now.strftime("%H:%M:%S")
puts "\n#{green}=>#{reset} #{bold}[#{timestamp}] #{string}#{reset}\n\n"
end
def command_description
<<-EOS
DESCRIPTION
'watchful' watches a path for changes, and when a file changes, it
runs the specified command.
The default watch path is the current working directory.
The default execution path is the watch path
Use '^C' to exit.
EXAMPLES
To watch the current directory, you could do
watchful -c mycommand
To watch a path, you could do
watchful -p /path/to/directory -c mycommand
To run the command in a different path, you could do
watchful -p /path/to/content --command-path /command/path -c mycommand
To run a command with arguments, you could do
watchful -c "mycommand -h"
To watch only a file or type of file, you could do
watchful --only README.md -c "bash build.bash"
watchful --only "^.*\\.md$" -c "bash build.bash"
watchful --only "notes\\/.*md$|css\\/.*html$|js\\/.*html$" -c "./build.bash"
To exclude a file, type of file, or path, you could do
watchful --exclude README.md -c "bash build.bash"
watchful --exclude "^.*\\.md$" -c "bash build.bash"
EOS
end
def parse_options
OptionParser.new do |opts|
opts.banner = "Usage: watchful [options] command"
opts.on("-p", "--path PATH", "Watch a specified path (default: current dir)") do |path|
path = expand_path path
validate_path path
@settings.path = path
end
opts.on("--only REGEX", "Watch files that only match regex") do |regex|
@settings.only_list.push regex
fail 'only a single "only" allowed' if @settings.only_list.size > 1
end
opts.on("--exclude REGEX", "Excludes files and paths that match regex") do |regex|
@settings.exclude_list.push regex
end
opts.on("-c", "--command COMMAND", "Run command when files in the path change") do |command|
@settings.command = command
end
opts.on("--command-path PATH", "Run command in specified path (default: watch path)") do |path|
path = expand_path path
validate_path path
@settings.command_path = path
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
puts command_description
exit
end
opts.on_tail("--version", "Show version") do
puts Watchful::VERSION
exit
end
end.parse!
if settings.command.nil?
STDERR.puts "missing command"
exit!
end
rescue => e
STDERR.puts "#{e.message}"
exit!
end
def run
handle_signals
parse_options
run_command
watch
end
end
end
Watchful::Command.instance.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment