changed watch deletions option to watch_all_modifiactions, Merge branch 'master' of git://github.com/guard/guard

Conflicts:
	lib/guard.rb
	lib/guard/cli.rb
	lib/guard/listener.rb
This commit is contained in:
Darren Pearce 2011-09-21 16:57:40 -06:00
commit 432d4a0991
22 changed files with 1153 additions and 262 deletions

2
.gitignore vendored
View File

@ -1,9 +1,11 @@
pkg/*
doc/*
*.gem
*.rbc
.*.swp
*.bak
.bundle
.yardoc
Gemfile.lock
## MAC OS

11
.yardopts Normal file
View File

@ -0,0 +1,11 @@
--title 'Guard Documentation'
--readme README.md
--markup markdown
--markup-provider kramdown
--private
--protected
--output-dir ./doc
lib/**/*.rb
-
CHANGELOG.md
LICENSE

View File

@ -13,12 +13,10 @@ require 'rbconfig'
if RbConfig::CONFIG['target_os'] =~ /darwin/i
gem 'rb-fsevent', '>= 0.4.0', :require => false
gem 'growl', '~> 1.0.3', :require => false
end
if RbConfig::CONFIG['target_os'] =~ /linux/i
elsif RbConfig::CONFIG['target_os'] =~ /linux/i
gem 'rb-inotify', '>= 0.8.5', :require => false
gem 'libnotify', '~> 0.1.3', :require => false
end
if RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
elsif RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
gem 'win32console', :require => false
gem 'rb-fchange', '>= 0.0.2', :require => false
gem 'rb-notifu', '>= 0.0.4', :require => false

View File

@ -14,11 +14,13 @@ Gem::Specification.new do |s|
s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = 'guard'
s.add_dependency 'thor', '~> 0.14.6'
s.add_development_dependency 'bundler'
s.add_development_dependency 'rspec', '~> 2.6.0'
s.add_development_dependency 'guard-rspec', '~> 0.3.1'
s.add_dependency 'thor', '~> 0.14.6'
s.add_development_dependency 'yard', '~> 0.7.2'
s.add_development_dependency 'kramdown', '~> 0.13.3'
s.files = Dir.glob('{bin,images,lib}/**/*') + %w[CHANGELOG.md LICENSE man/guard.1 man/guard.1.html README.md]
s.executable = 'guard'

View File

@ -1,3 +1,6 @@
# Guard is the main module for all Guard related modules and classes.
# Also other Guard implementation should use this namespace.
#
module Guard
autoload :UI, 'guard/ui'
@ -12,7 +15,16 @@ module Guard
class << self
attr_accessor :options, :guards, :groups, :interactor, :listener
# initialize this singleton
# Initialize the Guard singleton.
#
# @param [Hash] options the Guard options.
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [String] watchdir the director to watch
# @option options [String] guardfile the path to the Guardfile
# @optuin options [Boolean] watch_all_modifications watches all file modifications if true
def setup(options = {})
@options = options
@guards = []
@ -20,8 +32,8 @@ module Guard
@interactor = Interactor.new
@listener = Listener.select_and_init(@options[:watchdir] ? File.expand_path(@options[:watchdir]) : Dir.pwd,options)
@watch_deletions = options[:deletions]
@options[:notify] && ENV["GUARD_NOTIFY"] != 'false' ? Notifier.turn_on : Notifier.turn_off
@watch_all_modifications = options[:watch_all_modifications]
@options[:notify] && ENV['GUARD_NOTIFY'] != 'false' ? Notifier.turn_on : Notifier.turn_off
UI.clear if @options[:clear]
debug_command_execution if @options[:debug]
@ -29,6 +41,17 @@ module Guard
self
end
# Start Guard by evaluate the `Guardfile`, initialize the declared Guards
# and start the available file change listener.
#
# @param [Hash] options the Guard options.
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [String] watchdir the director to watch
# @option options [String] guardfile the path to the Guardfile
#
def start(options = {})
setup(options)
@ -46,41 +69,51 @@ module Guard
listener.start
end
# Stop Guard listening to file changes
#
def stop
UI.info "Bye bye...", :reset => true
UI.info 'Bye bye...', :reset => true
listener.stop
guards.each { |guard| supervised_task(guard, :stop) }
abort
end
# Reload all Guards currently enabled.
#
def reload
run do
guards.each { |guard| supervised_task(guard, :reload) }
end
end
# Trigger `run_all` on all Guards currently enabled.
#
def run_all
run do
guards.each { |guard| supervised_task(guard, :run_all) }
end
end
# Pause Guard listening to file changes.
#
def pause
if listener.locked
UI.info "Un-paused files modification listening", :reset => true
UI.info 'Un-paused files modification listening', :reset => true
listener.clear_changed_files
listener.unlock
else
UI.info "Paused files modification listening", :reset => true
UI.info 'Paused files modification listening', :reset => true
listener.lock
end
end
# Trigger `run_on_change` on all Guards currently enabled and
#
def run_on_change(files)
run do
guards.each do |guard|
paths = Watcher.match_files(guard, files)
if @watch_deletions
if @watch_all_modifications
unless paths.empty?
UI.debug "#{guard.class.name}#run_on_change with #{paths.inspect}"
supervised_task(guard, :run_on_change, paths.select {|f| !f.start_with?('!') })
@ -100,6 +133,11 @@ module Guard
end
end
# Run a block where the listener and the interactor is
# blocked.
#
# @yield the block to run
#
def run
listener.lock
interactor.lock
@ -112,24 +150,40 @@ module Guard
listener.unlock
end
# Let a guard execute its task but
# fire it if his work leads to a system failure
# Let a Guard execute its task, but fire it
# if his work leads to a system failure.
#
# @param [Guard::Guard] the guard to execute
# @param [Symbol] task_to_supervise the task to run
# @param [Array] args the arguments for the task
# @return [Boolean, Exception] the result of the Guard
#
def supervised_task(guard, task_to_supervise, *args)
guard.hook("#{ task_to_supervise }_begin", *args)
result = guard.send(task_to_supervise, *args)
guard.hook("#{ task_to_supervise }_end", result)
result
rescue Exception => ex
UI.error("#{ guard.class.name } failed to achieve its <#{ task_to_supervise.to_s }>, exception was:" +
"\n#{ ex.class }: #{ ex.message }\n#{ ex.backtrace.join("\n") }")
guards.delete guard
UI.info("\n#{ guard.class.name } has just been fired")
return ex
ex
end
# Add a Guard to use.
#
# @param [String] name the Guard name
# @param [Array<Watcher>] watchers the list of declared watchers
# @param [Array<Hash>] callbacks the list of callbacks
# @param [Hash] the Guard options
#
def add_guard(name, watchers = [], callbacks = [], options = {})
if name.to_sym == :ego
UI.deprecation("Guard::Ego is now part of Guard. You can remove it from your Guardfile.")
UI.deprecation('Guard::Ego is now part of Guard. You can remove it from your Guardfile.')
else
guard_class = get_guard_class(name)
callbacks.each { |callback| Hook.add_callback(callback[:listener], guard_class, callback[:events]) }
@ -137,10 +191,19 @@ module Guard
end
end
# Add a Guard group.
#
# @param [String] name the group name
#
def add_group(name)
@groups << name.to_sym unless name.nil?
end
# Tries to load the Guard main class.
#
# @param [String] name the name of the Guard
# @return [Class, nil] the loaded class
#
def get_guard_class(name)
name = name.to_s
try_require = false
@ -161,6 +224,11 @@ module Guard
end
end
# Locate a path to a Guard gem.
#
# @param [String] name the name of the Guard without the prefix `guard-`
# @return [String] the full path to the Guard gem
#
def locate_guard(name)
if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
Gem::Specification.find_by_name("guard-#{ name }").full_gem_path
@ -171,8 +239,10 @@ module Guard
UI.error "Could not find 'guard-#{ name }' gem path."
end
##
# Returns a list of guard Gem names installed locally.
#
# @return [Array<String>] a list of guard gem names
#
def guard_gem_names
if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
Gem::Specification.find_all.select { |x| x.name =~ /^guard-/ }
@ -181,6 +251,9 @@ module Guard
end.map { |x| x.name.sub /^guard-/, '' }
end
# Adds a command logger in debug mode. This wraps common command
# execution functions and logs the executed command before execution.
#
def debug_command_execution
Kernel.send(:alias_method, :original_system, :system)
Kernel.send(:define_method, :system) do |command, *args|
@ -188,8 +261,8 @@ module Guard
original_system command, *args
end
Kernel.send(:alias_method, :original_backtick, :"`")
Kernel.send(:define_method, :"`") do |command|
Kernel.send(:alias_method, :original_backtick, :'`')
Kernel.send(:define_method, :'`') do |command|
::Guard::UI.debug "Command execution: #{ command }"
original_backtick command
end

View File

@ -2,90 +2,178 @@ require 'thor'
require 'guard/version'
module Guard
# Guard command line interface managed by [Thor](https://github.com/wycats/thor).
# This is the main interface to Guard that is called by the Guard binary at `bin/guard`.
#
class CLI < Thor
default_task :start
method_option :clear, :type => :boolean, :default => false, :aliases => '-c', :banner => "Auto clear shell before each change/run_all/reload"
method_option :notify, :type => :boolean, :default => true, :aliases => '-n', :banner => "Notifications feature (growl/libnotify)"
method_option :debug, :type => :boolean, :default => false, :aliases => '-d', :banner => "Print debug messages"
method_option :group, :type => :array, :default => [], :aliases => '-g', :banner => "Run only the passed groups"
method_option :watchdir, :type => :string, :aliases => '-w', :banner => "Specify the directory to watch"
method_option :guardfile, :type => :string, :aliases => '-G', :banner => "Specify a Guardfile"
method_option :watch_moves_deletions, :type => :boolean, :default => false, :aliases => '-D', :banner => "Watch for moved or deleted files"
desc 'start', 'Starts Guard'
desc "start", "Starts Guard"
method_option :clear,
:type => :boolean,
:default => false,
:aliases => '-c',
:banner => 'Auto clear shell before each change/run_all/reload'
method_option :notify,
:type => :boolean,
:default => true,
:aliases => '-n',
:banner => 'Notifications feature (growl/libnotify)'
method_option :debug,
:type => :boolean,
:default => false,
:aliases => '-d',
:banner => 'Print debug messages'
method_option :group,
:type => :array,
:default => [],
:aliases => '-g',
:banner => 'Run only the passed groups'
method_option :watchdir,
:type => :string,
:aliases => '-w',
:banner => 'Specify the directory to watch'
method_option :guardfile,
:type => :string,
:aliases => '-G',
:banner => 'Specify a Guardfile'
method_option :watch_all_modifications,
:type => :boolean,
:default => false,
:aliases => '-A',
:banner => "Watch for all file modifications including moves and deletions"
# Start Guard by initialize the defined Guards and watch the file system.
# This is the default task, so calling `guard` is the same as calling `guard start`.
#
# @see Guard.start
#
def start
::Guard.start(options)
end
desc "list", "Lists guards that can be used with init"
desc 'list', 'Lists guards that can be used with init'
# List the Guards that are available for use in your system and marks
# those that are currently used in your `Guardfile`.
#
# @example guard list output
#
# Available guards:
# bundler *
# livereload
# ronn
# rspec *
# spork
#
# See also https://github.com/guard/guard/wiki/List-of-available-Guards
# * denotes ones already in your Guardfile
#
# @see Guard::DslDescriber
#
def list
::Guard::DslDescriber.evaluate_guardfile(options)
installed = []
::Guard::DslDescriber.guardfile_structure.each do |group|
group[:guards].each {|x| installed << x[:name]} if group[:guards]
Guard::DslDescriber.evaluate_guardfile(options)
installed = Guard::DslDescriber.guardfile_structure.inject([]) do |installed, group|
group[:guards].each { |guard| installed << guard[:name] } if group[:guards]
installed
end
::Guard::UI.info "Available guards:"
::Guard::guard_gem_names.sort.each do |name|
if installed.include? name
::Guard::UI.info " #{name} *"
else
::Guard::UI.info " #{name}"
end
end
::Guard::UI.info ' '
::Guard::UI.info "See also https://github.com/guard/guard/wiki/List-of-available-Guards"
::Guard::UI.info "* denotes ones already in your Guardfile"
Guard::UI.info 'Available guards:'
Guard::guard_gem_names.sort.uniq.each do |name|
Guard::UI.info " #{ name } #{ installed.include?(name) ? '*' : '' }"
end
desc "version", "Prints Guard's version"
def version
::Guard::UI.info "Guard version #{Guard::VERSION}"
Guard::UI.info ' '
Guard::UI.info 'See also https://github.com/guard/guard/wiki/List-of-available-Guards'
Guard::UI.info '* denotes ones already in your Guardfile'
end
desc 'version', 'Show the Guard version'
map %w(-v --version) => :version
desc "init [GUARD]", "Generates a Guardfile into the current working directory, or insert the given GUARD in an existing Guardfile"
def init(guard_name = nil)
if !File.exist?("Guardfile")
puts "Writing new Guardfile to #{Dir.pwd}/Guardfile"
FileUtils.cp(File.expand_path('../templates/Guardfile', __FILE__), 'Guardfile')
elsif guard_name.nil?
::Guard::UI.error "Guardfile already exists at #{Dir.pwd}/Guardfile"
exit 1
# Shows the current version of Guard.
#
# @see Guard::VERSION
#
def version
Guard::UI.info "Guard version #{ Guard::VERSION }"
end
desc 'init [GUARD]', 'Generates a Guardfile at the current working directory, or insert the given GUARD to an existing Guardfile'
# Appends the Guard template to the `Guardfile`, or creates an initial
# `Guardfile` when no Guard name is passed,
#
# @param [String] guard_name the name of the Guard to initialize
#
def init(guard_name = nil)
if guard_name
guard_class = ::Guard.get_guard_class(guard_name)
guard_class.init(guard_name)
end
end
desc "show", "Show all defined Guards and their options"
def show
::Guard::DslDescriber.evaluate_guardfile(options)
::Guard::DslDescriber.guardfile_structure.each do |group|
if !group[:guards].empty?
if group[:group]
::Guard::UI.info "Group #{group[:group]}:"
else
::Guard::UI.info "(global):"
if File.exist?('Guardfile')
puts 'Writing new Guardfile to #{Dir.pwd}/Guardfile'
FileUtils.cp(File.expand_path('../templates/Guardfile', __FILE__), 'Guardfile')
else
Guard::UI.error "Guardfile already exists at #{ Dir.pwd }/Guardfile"
exit 1
end
end
end
desc 'show', 'Show all defined Guards and their options'
map %w(-T) => :show
# Shows all Guards and their options that are defined in
# the `Guardfile`.
#
# @example guard show output
#
# (global):
# bundler
# coffeescript: input => "app/assets/javascripts", noop => true
# jasmine
# rspec: cli => "--fail-fast --format Fuubar
#
# @see Guard::DslDescriber
#
def show
Guard::DslDescriber.evaluate_guardfile(options)
Guard::DslDescriber.guardfile_structure.each do |group|
unless group[:guards].empty?
if group[:group]
Guard::UI.info "Group #{ group[:group] }:"
else
Guard::UI.info '(global):'
end
group[:guards].each do |guard|
line = " #{ guard[:name] }"
if !guard[:options].empty?
line += ": #{guard[:options].collect { |k, v| "#{k} => #{v.inspect}" }.join(", ")}"
unless guard[:options].empty?
line += ": #{ guard[:options].collect { |k, v| "#{ k } => #{ v.inspect }" }.join(', ') }"
end
::Guard::UI.info line
Guard::UI.info line
end
end
end
::Guard::UI.info ''
Guard::UI.info ''
end
map %w(-T) => :show
end
end

View File

@ -1,54 +1,152 @@
module Guard
# The DSL class provides the methods that are used in each `Guardfile` to describe
# the behaviour of Guard.
#
# The main keywords of the DSL are `guard` and `watch`, which are necessary to define
# which Guards are used a what file changes they are watching.
#
# Optionally you can group the Guards with the `group` keyword and ignore certain paths
# with the `ignore_paths` keyword.
#
# A more advanced DSL use is the `callback` keyword, that allows you to execute arbitrary
# code before or after any of the `start`, `stop`, `reload`, `run_all` and `run_on_change`
# guards' method. You can even insert more hooks inside these methods.
# Please [checkout the Wiki page](https://github.com/guard/guard/wiki/Hooks-and-callbacks) for more details.
#
# The DSL will also evaluate normal Ruby code.
#
# There are two possible locations for the `Guardfile`:
# - The `Guardfile` in the current directory where Guard has been started
# - The `.Guardfile` in your home directory.
#
# In addition, if a user configuration `.guard.rb` in your home directory is found, it will
# be appended to the current project `Guardfile`.
#
# @example A sample of a complex Guardfile
# group 'frontend' do
# guard 'passenger', :ping => true do
# watch('config/application.rb')
# watch('config/environment.rb')
# watch(%r{^config/environments/.+\.rb})
# watch(%r{^config/initializers/.+\.rb})
# end
#
# guard 'livereload', :apply_js_live => false do
# watch(%r{^app/.+\.(erb|haml)})
# watch(%r{^app/helpers/.+\.rb})
# watch(%r{^public/javascripts/.+\.js})
# watch(%r{^public/stylesheets/.+\.css})
# watch(%r{^public/.+\.html})
# watch(%r{^config/locales/.+\.yml})
# end
# end
#
# group 'backend' do
# # Reload the bundle when the Gemfile is modified
# guard 'bundler' do
# watch('Gemfile')
# end
#
# # for big project you can fine tune the "timeout" before Spork's launch is considered failed
# guard 'spork', :wait => 40 do
# watch('Gemfile')
# watch('config/application.rb')
# watch('config/environment.rb')
# watch(%r{^config/environments/.+\.rb})
# watch(%r{^config/initializers/.+\.rb})
# watch('spec/spec_helper.rb')
# end
#
# # use RSpec 2, from the system's gem and with some direct RSpec CLI options
# guard 'rspec', :version => 2, :cli => "--color --drb -f doc", :bundler => false do
# watch('spec/spec_helper.rb') { "spec" }
# watch('app/controllers/application_controller.rb') { "spec/controllers" }
# watch('config/routes.rb') { "spec/routing" }
# watch(%r{^spec/support/(controllers|acceptance)_helpers\.rb}) { |m| "spec/#{m[1]}" }
# watch(%r{^spec/.+_spec\.rb})
#
# watch(%r{^app/controllers/(.+)_(controller)\.rb}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
#
# watch(%r{^app/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
# watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
# end
# end
#
class Dsl
class << self
@@options = nil
# Evaluate the DSL methods in the `Guardfile`.
#
# @param [Hash] options the Guard options
# @option options [Array<Symbol,String>] groups the groups to evaluate
# @option options [String] guardfile the path to a valid Guardfile
# @option options [String] guardfile_contents a string representing the content of a valid Guardfile
# @raise [ArgumentError] when options are not a Hash
#
def evaluate_guardfile(options = {})
options.is_a?(Hash) or raise ArgumentError.new("evaluate_guardfile not passed a Hash!")
raise ArgumentError.new('No option hash passed to evaluate_guardfile!') unless options.is_a?(Hash)
@@options = options.dup
fetch_guardfile_contents
instance_eval_guardfile(guardfile_contents_with_user_config)
UI.error "No guards found in Guardfile, please add at least one." if !::Guard.guards.nil? && ::Guard.guards.empty?
UI.error 'No guards found in Guardfile, please add at least one.' if !::Guard.guards.nil? && ::Guard.guards.empty?
end
# Reevaluate the Guardfile to update the current Guard configuration
# when the `Guardfile` has been changed after Guard is started.
#
def reevaluate_guardfile
::Guard.guards.clear
@@options.delete(:guardfile_contents)
Dsl.evaluate_guardfile(@@options)
msg = "Guardfile has been re-evaluated."
msg = 'Guardfile has been re-evaluated.'
UI.info(msg)
Notifier.notify(msg)
end
# Evaluate the content of the `Guardfile`.
#
# @param [String] contents the content to evaluate.
#
def instance_eval_guardfile(contents)
begin
new.instance_eval(contents, @@options[:guardfile_path], 1)
rescue
UI.error "Invalid Guardfile, original error is:\n#{ $! }"
exit 1
end
end
# Test if the current `Guardfile` contains a specific Guard.
#
# @param [String] guard_name the name of the Guard
# @return [Boolean] whether the Guard has been declared
#
def guardfile_include?(guard_name)
guardfile_contents.match(/^guard\s*\(?\s*['":]#{ guard_name }['"]?/)
end
# Read the current `Guardfile` content.
#
# @param [String] the path to the Guardfile
#
def read_guardfile(guardfile_path)
begin
@@options[:guardfile_path] = guardfile_path
@@options[:guardfile_contents] = File.read(guardfile_path)
rescue
UI.error("Error reading file #{ guardfile_path }")
exit 1
end
end
# Get the content to evaluate and stores it into
# the options as :guardfile_contents.
#
def fetch_guardfile_contents
# TODO: do we need .rc file interaction?
if @@options[:guardfile_contents]
UI.info "Using inline Guardfile."
UI.info 'Using inline Guardfile.'
@@options[:guardfile_path] = 'Inline Guardfile'
elsif @@options[:guardfile]
@ -64,7 +162,7 @@ module Guard
if File.exist?(guardfile_default_path)
read_guardfile(guardfile_default_path)
else
UI.error "No Guardfile found, please create one with `guard init`."
UI.error 'No Guardfile found, please create one with `guard init`.'
exit 1
end
end
@ -75,71 +173,179 @@ module Guard
end
end
# Get the content of the `Guardfile`.
#
# @return [String] the Guardfile content
#
def guardfile_contents
@@options ? @@options[:guardfile_contents] : ""
@@options ? @@options[:guardfile_contents] : ''
end
# Get the content of the `Guardfile` and the global
# user configuration file.
#
# @see #user_config_path
#
# @return [String] the Guardfile content
#
def guardfile_contents_with_user_config
config = File.read(user_config_path) if File.exist?(user_config_path)
[guardfile_contents, config].join("\n")
end
# Get the file path to the project `Guardfile`.
#
# @return [String] the path to the Guardfile
#
def guardfile_path
@@options ? @@options[:guardfile_path] : ""
@@options ? @@options[:guardfile_path] : ''
end
# Tests if the current `Guardfile` content is usable.
#
# @return [Boolean] if the Guardfile is usable
#
def guardfile_contents_usable?
guardfile_contents && guardfile_contents.size >= 'guard :a'.size # smallest guard-definition
end
# Gets the default path of the `Guardfile`.
# This returns the `Guardfile` from the current directory when existing,
# or the global `Guardfile` at the home directory.
#
# @return [String] the path to the Guardfile
#
def guardfile_default_path
File.exist?(local_guardfile_path) ? local_guardfile_path : home_guardfile_path
end
private
# The path to the `Guardfile` that is located at
# the directory where Guard has been started from.
#
# @param [String] the path to the local Guardfile
#
def local_guardfile_path
File.join(Dir.pwd, "Guardfile")
File.join(Dir.pwd, 'Guardfile')
end
# The path to the `.Guardfile` that is located at
# the users home directory.
#
# @param [String] the path to ~/.Guardfile
#
def home_guardfile_path
File.expand_path(File.join("~", ".Guardfile"))
File.expand_path(File.join('~', '.Guardfile'))
end
# The path to the user configuration `.guard.rb`
# that is located at the users home directory.
#
# @param [String] the path to ~/.guard.rb
#
def user_config_path
File.expand_path(File.join("~", ".guard.rb"))
File.expand_path(File.join('~', '.guard.rb'))
end
end
def group(name, &guard_definition)
# Declares a group of guards to be run with `guard start --group group_name`.
#
# @example Declare two groups of Guards
# group 'backend' do
# guard 'spork'
# guard 'rspec'
# end
#
# group 'frontend' do
# guard 'passenger'
# guard 'livereload'
# end
#
# @param [Symbol, String] name the group's name called from the CLI
# @yield a block where you can declare several guards
#
# @see Dsl#guard
# @see Guard::DslDescriber
#
def group(name)
@groups = @@options[:group] || []
name = name.to_sym
if guard_definition && (@groups.empty? || @groups.map(&:to_sym).include?(name))
if block_given? && (@groups.empty? || @groups.map(&:to_sym).include?(name))
@current_group = name
guard_definition.call
yield if block_given?
@current_group = nil
end
end
def guard(name, options = {}, &watch_and_callback_definition)
# Declare a guard to be used when running `guard start`.
#
# The name parameter is usually the name of the gem without
# the 'guard-' prefix.
#
# The available options are different for each Guard implementation.
#
# @example Declare a Guard
# guard 'rspec' do
# end
#
# @param [String] name the Guard name
# @param [Hash] options the options accepted by the Guard
# @yield a block where you can declare several watch patterns and actions
#
# @see Dsl#watch
# @see Guard::DslDescriber
#
def guard(name, options = {})
@watchers = []
@callbacks = []
watch_and_callback_definition.call if watch_and_callback_definition
yield if block_given?
options.update(:group => (@current_group || :default))
::Guard.add_guard(name.to_s.downcase.to_sym, @watchers, @callbacks, options)
end
# Define a pattern to be watched in order to run actions on file modification.
#
# @example Declare watchers for a Guard
# guard 'rspec' do
# watch('spec/spec_helper.rb')
# watch(%r{^.+_spec.rb})
# watch(%r{^app/controllers/(.+).rb}) { |m| 'spec/acceptance/#{m[1]}s_spec.rb' }
# end
#
# @param [String, Regexp] pattern the pattern to be watched by the guard
# @yield a block to be run when the pattern is matched
# @yieldparam [MatchData] m matches of the pattern
# @yieldreturn a directory, a filename, an array of directories / filenames, or nothing (can be an arbitrary command)
#
def watch(pattern, &action)
@watchers << ::Guard::Watcher.new(pattern, action)
end
# Define a callback to execute arbitary code before or after any of
# the `start`, `stop`, `reload`, `run_all` and `run_on_change` guards' method.
#
# @param [Array] args the callback arguments
# @yield a block with listeners
#
def callback(*args, &listener)
listener, events = args.size > 1 ? args : [listener, args[0]]
@callbacks << { :events => events, :listener => listener }
end
# Ignore certain paths globally.
#
# @example Ignore some paths
# ignore_paths .git, .svn
#
# @param [Array] paths the list of paths to ignore
#
def ignore_paths(*paths)
UI.info "Ignoring paths: #{ paths.join(', ') }"
::Guard.listener.ignore_paths.push(*paths)

View File

@ -1,28 +1,60 @@
require 'guard/dsl'
module Guard
# The DslDescriber overrides methods to create an internal structure
# of the Guardfile that is used in some inspection utility methods
# like the CLI commands `show` and `list`.
#
# @see Guard::DSL
# @see Guard::CLI
#
class DslDescriber < Dsl
@@guardfile_structure = [ { :guards => [] } ]
class << self
# Get the Guardfile structure.
#
# @return [Array<Hash>] the structure
#
def guardfile_structure
@@guardfile_structure
end
end
private
def group(name, &guard_definition)
@@guardfile_structure << { :group => name.to_sym, :guards => [] }
# Declares a group of guards.
#
# @param [String] name the group's name called from the CLI
# @yield a block where you can declare several guards
#
# @see Guard::Dsl
#
def group(name)
@@guardfile_structure << { :group => name.to_sym, :guards => [] }
@group = true
guard_definition.call
yield if block_given?
@group = false
end
def guard(name, options = {}, &watch_definition)
# Declare a guard.
#
# @param [String] name the Guard name
# @param [Hash] options the options accepted by the Guard
# @yield a block where you can declare several watch patterns and actions
#
# @see Guard::Dsl
#
def guard(name, options = {})
node = (@group ? @@guardfile_structure.last : @@guardfile_structure.first)
node[:guards] << { :name => name, :options => options }
end
end
end

View File

@ -1,15 +1,33 @@
module Guard
# Main class that every Guard implementation must subclass.
#
# Guard will trigger the `start`, `stop`, `reload`, `run_all` and `run_on_change`
# methods depending on user interaction and file modification.
#
# Each Guard should provide a template Guardfile located within the Gem
# at `lib/guard/guard-name/templates/Guardfile`.
#
class Guard
include Hook
attr_accessor :watchers, :options, :group
# initialize a Guard.
#
# @param [Array<Guard::Watcher>] watchers the Guard file watchers
# @param [Hash] options the custom Guard options.
#
def initialize(watchers = [], options = {})
@group = options.delete(:group) || :default
@watchers, @options = watchers, options
end
# Guardfile template needed inside guard gem
# Initialize the Guard. This will copy the Guardfile template inside the Guard gem.
# The template Guardfile must be located within the Gem at `lib/guard/guard-name/templates/Guardfile`.
#
# @param [String] name the name of the Guard
#
def self.init(name)
if ::Guard::Dsl.guardfile_include?(name)
::Guard::UI.info "Guardfile already includes #{ name } guard"
@ -25,31 +43,43 @@ module Guard
end
end
# ================
# = Guard method =
# ================
# Call once when guard starts
# Please override initialize method to init stuff
# Call once when guard starts. Please override initialize method to init stuff.
#
# @return [Boolean] Whether the start action was successful or not
#
def start
true
end
# Call once when guard quit
# Call once when guard quit.
#
# @return [Boolean] Whether the stop action was successful or not
#
def stop
true
end
# Should be mainly used for "reload" (really!) actions like reloading passenger/spork/bundler/...
# Should be used for "reload" (really!) actions like reloading passenger/spork/bundler/...
#
# @return [Boolean] Whether the reload action was successful or not
#
def reload
true
end
# Should be principally used for long action like running all specs/tests/...
# Should be used for long action like running all specs/tests/...
#
# @return [Boolean] Whether the run_all action was successful or not
#
def run_all
true
end
# Will be triggered when a file change matched a watcher.
#
# @param [Array<String>] paths the changes files or paths
# @return [Boolean] Whether the run_all action was successful or not
#
def run_on_change(paths)
true
end

View File

@ -1,32 +1,52 @@
module Guard
# Guard has a hook mechanism that allows you to insert callbacks for individual Guards.
# By default, each of the Guard instance methods has a "_begin" and an "_end" hook.
# For example, the Guard::Guard#start method has a :start_begin hook that is run immediately
# before Guard::Guard#start and a :start_end hook that is run immediately after Guard::Guard#start.
#
# Read more about [hooks and callbacks on the wiki](https://github.com/guard/guard/wiki/Hooks-and-callbacks).
#
module Hook
# The Hook module gets included.
#
# @param [Class] base the class that includes the module
#
def self.included(base)
base.send :include, InstanceMethods
end
# Instance methods that gets included in the base class.
#
module InstanceMethods
# When +event+ is a Symbol, #hook will generate a hook name
# by concatenating the method name from where #hook is called
# When +event+ is a Symbol, {#hook} will generate a hook name
# by concatenating the method name from where {#hook} is called
# with the given Symbol.
# Example:
#
# @example Add a hook with a Symbol
# def run_all
# hook :foo
# end
# Here, when #run_all is called, #hook will notify callbacks
#
# Here, when {Guard::Guard#run_all} is called, {#hook} will notify callbacks
# registered for the "run_all_foo" event.
#
# When +event+ is a String, #hook will directly turn the String
# When +event+ is a String, {#hook} will directly turn the String
# into a Symbol.
# Example:
#
# @example Add a hook with a String
# def run_all
# hook "foo_bar"
# end
# Here, when #run_all is called, #hook will notify callbacks
#
# When {Guard::Guard#run_all} is called, {#hook} will notify callbacks
# registered for the "foo_bar" event.
#
# +args+ parameter is passed as is to the callbacks registered
# for the given event.
# @param [Symbol, String] event the name of the Guard event
# @param [Array] args the parameters are passed as is to the callbacks registered for the given event.
#
def hook(event, *args)
hook_name = if event.is_a? Symbol
calling_method = caller[0][/`([^']*)'/, 1]
@ -42,10 +62,19 @@ module Guard
end
class << self
# Get all callbacks
#
def callbacks
@callbacks ||= Hash.new { |hash, key| hash[key] = [] }
end
# Add a callback.
#
# @param [Block] listener the listener to notify
# @param [Guard::Guard] guard_class the Guard class to add the callback
# @param [Array<Symbol>] events the events to register
#
def add_callback(listener, guard_class, events)
_events = events.is_a?(Array) ? events : [events]
_events.each do |event|
@ -53,19 +82,34 @@ module Guard
end
end
# Checks if a callback has been registered.
#
# @param [Block] listener the listener to notify
# @param [Guard::Guard] guard_class the Guard class to add the callback
# @param [Symbol] event the event to look for
#
def has_callback?(listener, guard_class, event)
callbacks[[guard_class, event]].include?(listener)
end
# Notify a callback.
#
# @param [Guard::Guard] guard_class the Guard class to add the callback
# @param [Symbol] event the event to trigger
# @param [Array] args the arguments for the listener
#
def notify(guard_class, event, *args)
callbacks[[guard_class, event]].each do |listener|
listener.call(guard_class, event, *args)
end
end
# Reset all callbacks
#
def reset_callbacks!
@callbacks = nil
end
end
end

View File

@ -1,14 +1,29 @@
module Guard
# The interactor reads user input and triggers
# specific action upon them unless its locked.
#
# Currently the following actions are implemented:
# - stop, quit, exit, s, q, e => Exit Guard
# - reload, r, z => Reload Guard
# - pause, p => Pause Guard
# - Everything else => Run all
#
class Interactor
attr_reader :locked
# Initialize the interactor in unlocked state.
#
def initialize
@locked = false
end
# Start the interactor in a own thread.
#
def start
return if ENV["GUARD_ENV"] == 'test'
Thread.new do
loop do
if (entry = $stdin.gets) && !@locked
@ -28,10 +43,14 @@ module Guard
end
end
# Lock the interactor.
#
def lock
@locked = true
end
# Unlock the interactor.
#
def unlock
@locked = false
end

View File

@ -8,42 +8,65 @@ module Guard
autoload :Windows, 'guard/listeners/windows'
autoload :Polling, 'guard/listeners/polling'
# The Listener is the base class for all listener
# implementations.
#
# @abstract
#
class Listener
DefaultIgnorePaths = %w[. .. .bundle .git log tmp vendor]
# Default paths that gets ignored by the listener
DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor]
attr_accessor :changed_files
attr_reader :directory, :ignore_paths, :locked
def self.select_and_init(*a)
# Select the appropriate listener implementation for the
# current OS and initializes it.
#
# @param [Array] args the arguments for the listener
# @return [Guard::Listener] the chosen listener
#
def self.select_and_init(*args)
if mac? && Darwin.usable?
Darwin.new(*a)
Darwin.new(*args)
elsif linux? && Linux.usable?
Linux.new(*a)
Linux.new(*args)
elsif windows? && Windows.usable?
Windows.new(*a)
Windows.new(*args)
else
UI.info "Using polling (Please help us to support your system better than that.)"
Polling.new(*a)
UI.info 'Using polling (Please help us to support your system better than that).'
Polling.new(*args)
end
end
# Initialize the listener.
#
# @param [String] directory the root directory to listen to
# @param [Hash] options the listener options
# @option options [Boolean] relativize_paths use only relative paths
# @option options [Array<String>] ignore_paths the paths to ignore by the listener
#
def initialize(directory = Dir.pwd, options = {})
@directory = directory.to_s
@sha1_checksums_hash = {}
@file_timestamp_hash = {}
@relativize_paths = options.fetch(:relativize_paths, true)
@watch_deletions = options.fetch(:deletions, false)
@changed_files = []
@locked = false
@ignore_paths = DefaultIgnorePaths
@ignore_paths = DEFAULT_IGNORE_PATHS
@ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
@watch_all_modifications = options.fetch(:watch_all_modifications, false)
update_last_event
start_reactor
end
# Start the listener thread.
#
def start_reactor
return if ENV["GUARD_ENV"] == 'test'
Thread.new do
loop do
if @changed_files != [] && !@locked
@ -57,38 +80,59 @@ module Guard
end
end
# Start watching the root directory.
#
def start
watch(@directory)
timestamp_files
end
# Stop listening for events.
#
def stop
end
# Lock the listener to ignore change events.
#
def lock
@locked = true
end
# Unlock the listener to listen again to change events.
#
def unlock
@locked = false
end
# Clear the list of changed files.
#
def clear_changed_files
@changed_files.clear
end
# Store a listener callback.
#
# @param [Block] callback the callback to store
#
def on_change(&callback)
@callback = callback
end
# Updates the timestamp of the last event.
#
def update_last_event
@last_event = Time.now
end
# Get the modified files.
#
# @param [Array<String>] dirs the watched directories
# @param [Hash] options the listener options
#
def modified_files(dirs, options = {})
last_event = @last_event
files = []
if @watch_deletions
if @watch_all_modifications
deleted_files = @file_timestamp_hash.collect do |path, ts|
unless File.exists?(path)
@sha1_checksums_hash.delete(path)
@ -103,37 +147,55 @@ module Guard
relativize_paths(files)
end
def worker
raise NotImplementedError, "should respond to #watch"
end
# register a directory to watch. must be implemented by the subclasses
# Register a directory to watch.
# Must be implemented by the subclasses.
#
# @param [String] directory the directory to watch
#
def watch(directory)
raise NotImplementedError, "do whatever you want here, given the directory as only argument"
end
# Get all files that are in the watched directory.
#
# @return [Array<String>] the list of files
#
def all_files
potentially_modified_files([@directory], :all => true)
end
# scopes all given paths to the current #directory
# Scopes all given paths to the current directory.
#
# @param [Array<String>] paths the paths to change
# @return [Array<String>] all paths now relative to the current dir
#
def relativize_paths(paths)
return paths unless relativize_paths?
paths.map do |path|
path.gsub(%r{#{@directory}/}, '')
#path.gsub(%r{^#{ @directory }/}, '')
end
end
# Use relative paths?
#
# @return [Boolean] whether to use relative or absolute paths
#
def relativize_paths?
!!@relativize_paths
end
# populate initial timestamp file hash to watch for deleted or moved files
def timestamp_files
all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_deletions
all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
end
# return children of the passed dirs that are not in the ignore_paths list
# Removes ignored paths from the directory list.
#
# @param [Array<String>] dirs the directory to listen to
# @param [Array<String>] ignore_paths the paths to ignore
# @return children of the passed dirs that are not in the ignore_paths list
#
def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
ignore_paths.include?(File.basename(path))
@ -142,6 +204,12 @@ module Guard
private
# Gets a list of files that are in the modified firectories.
#
# @param [Array<String>] dirs the list of directories
# @param [Hash] options the options
# @option options [Symbol] all whether to include all files
#
def potentially_modified_files(dirs, options = {})
paths = exclude_ignored_paths(dirs)
@ -159,9 +227,17 @@ module Guard
end
end
# Test if the file content has changed.
#
# Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
# both values down to the second for the comparison.
#
# ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
#
# @param [String] path the file path
# @param [Time] last_event the time of the last event
# @return [Boolean] Whether the file content has changed or not.
#
def file_modified?(path, last_event)
ctime = File.ctime(path).to_i
mtime = File.mtime(path).to_i
@ -170,7 +246,7 @@ module Guard
elsif mtime > last_event.to_i
set_sha1_checksums_hash(path, sha1_checksum(path))
true
elsif @watch_deletions
elsif @watch_all_modifications
ts = file_timestamp(path)
if ts != @file_timestamp_hash[path]
set_file_timestamp_hash(path, ts)
@ -183,6 +259,12 @@ module Guard
false
end
# Tests if the file content has been modified by
# comparing the SHA1 checksum.
#
# @param [String] path the file path
# @param [String] sha1_checksum the checksum of the file
#
def file_content_modified?(path, sha1_checksum)
if @sha1_checksums_hash[path] != sha1_checksum
set_sha1_checksums_hash(path, sha1_checksum)
@ -196,6 +278,11 @@ module Guard
@file_timestamp_hash[path] = file_timestamp
end
# Set the current checksum of a file.
#
# @param [String] path the file path
# @param [String] sha1_checksum the checksum of the file
#
def set_sha1_checksums_hash(path, sha1_checksum)
@sha1_checksums_hash[path] = sha1_checksum
end
@ -204,18 +291,35 @@ module Guard
File.mtime(path).to_i
end
# Calculates the SHA1 checksum of a file.
#
# @param [String] path the path to the file
# @return [String] the SHA1 checksum
#
def sha1_checksum(path)
Digest::SHA1.file(path).to_s
end
# Test if the OS is Mac OS X.
#
# @return [Boolean] Whether the OS is Mac OS X
#
def self.mac?
RbConfig::CONFIG['target_os'] =~ /darwin/i
end
# Test if the OS is Linux.
#
# @return [Boolean] Whether the OS is Linux
#
def self.linux?
RbConfig::CONFIG['target_os'] =~ /linux/i
end
# Test if the OS is Windows.
#
# @return [Boolean] Whether the OS is Windows
#
def self.windows?
RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
end

View File

@ -1,41 +1,60 @@
module Guard
# Listener implementation for Mac OS X FSEvents.
#
class Darwin < Listener
# Initialize the Listener.
#
def initialize(*)
super
@fsevent = FSEvent.new
end
def worker
@fsevent
end
# Start the listener.
#
def start
super
worker.run
end
# Stop the listener.
#
def stop
super
worker.stop
end
# Check if the listener is usable on the current OS.
#
# @return [Boolean] whether usable or not
#
def self.usable?
require 'rb-fsevent'
if !defined?(FSEvent::VERSION) || (defined?(Gem::Version) &&
Gem::Version.new(FSEvent::VERSION) < Gem::Version.new('0.4.0'))
UI.info "Please update rb-fsevent (>= 0.4.0)"
UI.info 'Please update rb-fsevent (>= 0.4.0)'
false
else
true
end
rescue LoadError
UI.info "Please install rb-fsevent gem for Mac OSX FSEvents support"
UI.info 'Please install rb-fsevent gem for Mac OSX FSEvents support'
false
end
private
# Get the listener worker.
#
def worker
@fsevent
end
# Watch the given directory for file changes.
#
# @param [String] directory the directory to watch
#
def watch(directory)
worker.watch(directory) do |modified_dirs|
files = modified_files(modified_dirs)

View File

@ -1,6 +1,11 @@
module Guard
# Listener implementation for Linux inotify.
#
class Linux < Listener
# Initialize the Listener.
#
def initialize(*)
super
@inotify = INotify::Notifier.new
@ -8,44 +13,53 @@ module Guard
@latency = 0.5
end
# Start the listener.
#
def start
@stop = false
super
watch_change unless watch_change?
end
# Stop the listener.
#
def stop
super
@stop = true
sleep(@latency)
end
# Check if the listener is usable on the current OS.
#
# @return [Boolean] whether usable or not
#
def self.usable?
require 'rb-inotify'
if !defined?(INotify::VERSION) || (defined?(Gem::Version) &&
Gem::Version.new(INotify::VERSION.join('.')) < Gem::Version.new('0.8.5'))
UI.info "Please update rb-inotify (>= 0.8.5)"
UI.info 'Please update rb-inotify (>= 0.8.5)'
false
else
true
end
rescue LoadError
UI.info "Please install rb-inotify gem for Linux inotify support"
UI.info 'Please install rb-inotify gem for Linux inotify support'
false
end
def watch_change?
!!@watch_change
end
private
# Get the listener worker.
#
def worker
@inotify
end
# Watch the given directory for file changes.
#
# @param [String] directory the directory to watch
#
def watch(directory)
# The event selection is based on https://github.com/guard/guard/wiki/Analysis-of-inotify-events-for-different-editors
worker.watch(directory, :recursive, :attrib, :create, :move_self, :close_write) do |event|
unless event.name == "" # Event on root directory
@files << event.absolute_name
@ -54,6 +68,16 @@ module Guard
rescue Interrupt
end
# Test if inotify is watching for changes.
#
# @return [Boolean] whether inotify is active or not
#
def watch_change?
!!@watch_change
end
# Watch for file system changes.
#
def watch_change
@watch_change = true
until @stop

View File

@ -1,24 +1,46 @@
module Guard
# Polling listener that works cross-plattform and
# has no dependencies. This is the listener that
# uses the most CPU processing power and has higher
# File IO that the other implementations.
#
class Polling < Listener
# Initialize the Listener.
#
def initialize(*)
super
@latency = 1.5
end
# Start the listener.
#
def start
@stop = false
super
watch_change
end
# Stop the listener.
#
def stop
super
@stop = true
end
# Watch the given directory for file changes.
#
# @param [String] directory the directory to watch
#
def watch(directory)
@existing = all_files
end
private
# Watch for file system changes.
#
def watch_change
until @stop
start = Time.now.to_f
@ -29,9 +51,5 @@ module Guard
end
end
def watch(directory)
@existing = all_files
end
end
end

View File

@ -1,35 +1,48 @@
module Guard
# Listener implementation for Windows fchange.
#
class Windows < Listener
# Initialize the Listener.
#
def initialize(*)
super
@fchange = FChange::Notifier.new
end
# Start the listener.
#
def start
super
worker.run
end
# Stop the listener.
#
def stop
super
worker.stop
end
# Check if the listener is usable on the current OS.
#
# @return [Boolean] whether usable or not
#
def self.usable?
require 'rb-fchange'
true
rescue LoadError
UI.info "Please install rb-fchange gem for Windows file events support"
UI.info 'Please install rb-fchange gem for Windows file events support'
false
end
private
def worker
@fchange
end
# Watch the given directory for file changes.
#
# @param [String] directory the directory to watch
#
def watch(directory)
worker.watch(directory, :all_events, :recursive) do |event|
paths = [File.expand_path(event.watcher.path)]
@ -38,5 +51,11 @@ module Guard
end
end
# Get the listener worker.
#
def worker
@fchange
end
end
end

View File

@ -3,13 +3,28 @@ require 'pathname'
require 'guard/ui'
module Guard
# The notifier class handles cross-platform system notifications that supports:
# - Growl on Mac OS X
# - Libnotify on Linux
# - Notifu on Windows
#
module Notifier
# Application name as shown in the specific notification settings
APPLICATION_NAME = "Guard"
# Turn notifications of.
#
def self.turn_off
ENV["GUARD_NOTIFY"] = 'false'
end
# Turn notifications on. This tries to load the platform
# specific notification library.
#
# @return [Boolean] whether the notification could be enabled.
#
def self.turn_on
ENV["GUARD_NOTIFY"] = 'true'
case RbConfig::CONFIG['target_os']
@ -22,6 +37,15 @@ module Guard
end
end
# Show a message with the system notification.
#
# @see .image_path
#
# @param [String] the message to show
# @param [Hash] options the notification options
# @option options [Symbol, String] image the image symbol or path to an image
# @option options [String] title the notification title
#
def self.notify(message, options = {})
if enabled?
image = options.delete(:image) || :success
@ -38,12 +62,23 @@ module Guard
end
end
# Test if the notifications are enabled and available.
#
# @return [Boolean] whether the notifications are available
#
def self.enabled?
ENV["GUARD_NOTIFY"] == 'true'
end
private
# Send a message to Growl either with the growl gem or the growl_notify gem.
#
# @param [String] title the notification title
# @param [String] message the message to show
# @param [Symbol, String] the image to user
# @param [Hash] options the growl options
#
def self.notify_mac(title, message, image, options)
require_growl # need for guard-rspec formatter that is called out of guard scope
@ -61,18 +96,42 @@ module Guard
end
end
# Send a message to libnotify.
#
# @param [String] title the notification title
# @param [String] message the message to show
# @param [Symbol, String] the image to user
# @param [Hash] options the libnotify options
#
def self.notify_linux(title, message, image, options)
require_libnotify # need for guard-rspec formatter that is called out of guard scope
default_options = { :body => message, :summary => title, :icon_path => image_path(image), :transient => true }
Libnotify.show default_options.merge(options) if enabled?
end
# Send a message to notifu.
#
# @param [String] title the notification title
# @param [String] message the message to show
# @param [Symbol, String] the image to user
# @param [Hash] options the notifu options
#
def self.notify_windows(title, message, image, options)
require_rbnotifu # need for guard-rspec formatter that is called out of guard scope
default_options = { :message => message, :title => title, :type => image_level(image), :time => 3 }
Notifu.show default_options.merge(options) if enabled?
end
# Get the image path for an image symbol.
#
# Known symbols are:
# - failed
# - pending
# - success
#
# @param [Symbol] image the image name
# @return [String] the image path
#
def self.image_path(image)
images_path = Pathname.new(File.dirname(__FILE__)).join('../../images')
case image
@ -88,6 +147,11 @@ module Guard
end
end
# The notification level type for the given image.
#
# @param [Symbol] image the image
# @return [Symbol] the level
#
def self.image_level(image)
case image
when :failed
@ -101,6 +165,9 @@ module Guard
end
end
# Try to safely load growl and turns notifications
# off on load failure.
#
def self.require_growl
begin
require 'growl_notify'
@ -119,6 +186,9 @@ module Guard
UI.info "Please install growl_notify or growl gem for Mac OS X notification support and add it to your Gemfile"
end
# Try to safely load libnotify and turns notifications
# off on load failure.
#
def self.require_libnotify
require 'libnotify'
rescue LoadError
@ -126,11 +196,15 @@ module Guard
UI.info "Please install libnotify gem for Linux notification support and add it to your Gemfile"
end
# Try to safely load rb-notifu and turns notifications
# off on load failure.
#
def self.require_rbnotifu
require 'rb-notifu'
rescue LoadError
turn_off
UI.info "Please install rb-notifu gem for Windows notification support and add it to your Gemfile"
end
end
end

View File

@ -1,88 +1,92 @@
module Guard
# The UI class helps to format messages for the user.
#
module UI
ANSI_ESCAPE_BRIGHT = "1"
ANSI_ESCAPE_BLACK = "30"
ANSI_ESCAPE_RED = "31"
ANSI_ESCAPE_GREEN = "32"
ANSI_ESCAPE_YELLOW = "33"
ANSI_ESCAPE_BLUE = "34"
ANSI_ESCAPE_MAGENTA = "35"
ANSI_ESCAPE_CYAN = "36"
ANSI_ESCAPE_WHITE = "37"
ANSI_ESCAPE_BGBLACK = "40"
ANSI_ESCAPE_BGRED = "41"
ANSI_ESCAPE_BGGREEN = "42"
ANSI_ESCAPE_BGYELLOW = "43"
ANSI_ESCAPE_BGBLUE = "44"
ANSI_ESCAPE_BGMAGENTA = "45"
ANSI_ESCAPE_BGCYAN = "46"
ANSI_ESCAPE_BGWHITE = "47"
class << self
color_enabled = nil
# Show an info message.
#
# @param [String] message the message to show
# @param [Hash] options the options
# @option options [Boolean] reset whether to clean the output before
#
def info(message, options = { })
unless ENV["GUARD_ENV"] == "test"
unless ENV['GUARD_ENV'] == 'test'
reset_line if options[:reset]
puts color(message) if message != ''
end
end
# Show a red error message that is prefixed with ERROR.
#
# @param [String] message the message to show
# @param [Hash] options the options
# @option options [Boolean] reset whether to clean the output before
#
def error(message, options = { })
unless ENV["GUARD_ENV"] == "test"
unless ENV['GUARD_ENV'] == 'test'
reset_line if options[:reset]
puts color('ERROR: ', :red) + message
end
end
# Show a red deprecation message that is prefixed with DEPRECATION.
#
# @param [String] message the message to show
# @param [Hash] options the options
# @option options [Boolean] reset whether to clean the output before
#
def deprecation(message, options = { })
unless ENV["GUARD_ENV"] == "test"
unless ENV['GUARD_ENV'] == 'test'
reset_line if options[:reset]
puts color('DEPRECATION: ', :red) + message
end
end
# Show a debug message that is prefixed with DEBUG and a timestampe.
#
# @param [String] message the message to show
# @param [Hash] options the options
# @option options [Boolean] reset whether to clean the output before
#
def debug(message, options={ })
unless ENV["GUARD_ENV"] == "test"
unless ENV['GUARD_ENV'] == 'test'
reset_line if options[:reset]
puts color("DEBUG (#{Time.now.strftime('%T')}): ", :yellow) + message if ::Guard.options && ::Guard.options[:debug]
end
end
# Reset a line.
#
def reset_line
print(color_enabled? ? "\r\e[0m" : "\r\n")
end
# Clear the output.
#
def clear
system("clear;")
system('clear;')
end
private
# Reset a color sequence.
#
# @deprecated
# @param [String] text the text
#
def reset_color(text)
deprecation('UI.reset_color(text) is deprecated, please use color(text, "") instead.')
color(text, "")
end
def color(text, *color_options)
color_code = ""
color_options.each do |color_option|
color_option = color_option.to_s
if color_option != ""
if !(color_option =~ /\d+/)
color_option = const_get("ANSI_ESCAPE_#{color_option.upcase}")
end
color_code += ";" + color_option
end
end
color_enabled? ? "\e[0#{color_code}m#{text}\e[0m" : text
deprecation('UI.reset_color(text) is deprecated, please use color(text, ' ') instead.')
color(text, '')
end
# Checks if color output can be enabled.
#
# @return [Boolean] whether color is enabled or not
#
def color_enabled?
if @color_enabled.nil?
if RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
@ -102,9 +106,86 @@ module Guard
@color_enabled = true
end
end
@color_enabled
end
# Colorizes a text message. See the constant below for possible
# color_options parameters. You can pass :bright, a foreground
# and a background color.
#
# @example
# color('Hello World', :red, :bright)
#
# @param [String] the text to colorize
# @param [Array] color_options the color options
#
def color(text, *color_options)
color_code = ''
color_options.each do |color_option|
color_option = color_option.to_s
if color_option != ''
if !(color_option =~ /\d+/)
color_option = const_get("ANSI_ESCAPE_#{ color_option.upcase }")
end
color_code += ';' + color_option
end
end
color_enabled? ? "\e[0#{ color_code }m#{ text }\e[0m" : text
end
end
# bright color
ANSI_ESCAPE_BRIGHT = '1'
# black foreground color
ANSI_ESCAPE_BLACK = '30'
# red foreground color
ANSI_ESCAPE_RED = '31'
# green foreground color
ANSI_ESCAPE_GREEN = '32'
# yellow foreground color
ANSI_ESCAPE_YELLOW = '33'
# blue foreground color
ANSI_ESCAPE_BLUE = '34'
# magenta foreground color
ANSI_ESCAPE_MAGENTA = '35'
# cyan foreground color
ANSI_ESCAPE_CYAN = '36'
# white foreground color
ANSI_ESCAPE_WHITE = '37'
# black background color
ANSI_ESCAPE_BGBLACK = '40'
# red background color
ANSI_ESCAPE_BGRED = '41'
# green background color
ANSI_ESCAPE_BGGREEN = '42'
# yellow background color
ANSI_ESCAPE_BGYELLOW = '43'
# blue background color
ANSI_ESCAPE_BGBLUE = '44'
# magenta background color
ANSI_ESCAPE_BGMAGENTA = '45'
# cyan background color
ANSI_ESCAPE_BGCYAN = '46'
# white background color
ANSI_ESCAPE_BGWHITE = '47'
end
end

View File

@ -1,3 +1,6 @@
module Guard
VERSION = "0.7.0" unless defined? Guard::VERSION
unless defined? Guard::VERSION
# The current gem version of Guard
VERSION = '0.7.0'
end
end

View File

@ -1,7 +1,18 @@
module Guard
# The watcher defines a RegEx that will be matched against file system modifications.
# When a watcher matches a change, an optional action block is executed to enable
# processing the file system change result.
#
class Watcher
attr_accessor :pattern, :action
# Initialize a file watcher.
#
# @param [String, Regexp] pattern the pattern to be watched by the guard
# @param [Block] action the action to execute before passing the result to the Guard
#
def initialize(pattern, action = nil)
@pattern, @action = pattern, action
@@warning_printed ||= false
@ -10,14 +21,25 @@ module Guard
if @pattern.is_a?(String) && @pattern =~ /(^(\^))|(>?(\\\.)|(\.\*))|(\(.*\))|(\[.*\])|(\$$)/
unless @@warning_printed
UI.info "*"*20 + "\nDEPRECATION WARNING!\n" + "*"*20
UI.info "You have strings in your Guardfile's watch patterns that seem to represent regexps.\nGuard matchs String with == and Regexp with Regexp#match.\nYou should either use plain String (without Regexp special characters) or real Regexp.\n"
UI.info <<-MSG
You have a string in your Guardfile watch patterns that seem to represent a Regexp.
Guard matches String with == and Regexp with Regexp#match.
You should either use plain String (without Regexp special characters) or real Regexp.
MSG
@@warning_printed = true
end
UI.info "\"#{@pattern}\" has been converted to #{ Regexp.new(@pattern).inspect }\n"
@pattern = Regexp.new(@pattern)
end
end
# Finds the files that matches a Guard.
#
# @param [Guard::Guard] guard the guard which watchers are used
# @param [Array<String>] files the changed files
# @return [Array<String>] the matched files
#
def self.match_files(guard, files)
guard.watchers.inject([]) do |paths, watcher|
files.each do |file|
@ -30,10 +52,17 @@ module Guard
end
end
end
paths.flatten.map { |p| p.to_s }
end
end
# Test if a file would be matched by any of the Guards watchers.
#
# @param [Array<Guard::Guard>] guards the guards to use the watchers from
# @param [Array<String>] files the files to test
# @return [Boolean] Whether a file matches
#
def self.match_files?(guards, files)
guards.any? do |guard|
guard.watchers.any? do |watcher|
@ -42,6 +71,11 @@ module Guard
end
end
# Test the watchers pattern against a file.
#
# @param [String] file the file to test
# @return [Boolean] whether the given file is matched
#
def match_file?(file)
if @pattern.is_a?(Regexp)
file.match(@pattern)
@ -50,10 +84,20 @@ module Guard
end
end
# Test if any of the files is the Guardfile.
#
# @param [Array<String>] the files to test
# @return [Boolean] whether one of these files is the Guardfile
#
def self.match_guardfile?(files)
files.any? { |file| "#{ Dir.pwd }/#{ file }" == Dsl.guardfile_path }
end
# Executes a watcher action.
#
# @param [String, MatchData] the matched path or the match from the Regex
# @return [String] the final paths
#
def call_action(matches)
begin
@action.arity > 0 ? @action.call(matches) : @action.call

View File

@ -134,12 +134,12 @@ describe Guard::Listener do
end
end
context "without watch_deletions" do
context "without watch_all_modifications" do
after { FileUtils.touch(file3) }
it "defaults to false" do
subject.instance_variable_get(:@watch_deletions).should eql false
subject.instance_variable_get(:@watch_all_modifications).should eql false
end
it "it should not track deleted files" do
@ -152,8 +152,8 @@ describe Guard::Listener do
end
end
context "with watch_deletions" do
subject { described_class.new(Dir.pwd, :deletions=>true) }
context "with watch_all_modifications" do
subject { described_class.new(Dir.pwd, :watch_all_modifications=>true) }
before :each do
subject.timestamp_files
@ -166,7 +166,7 @@ describe Guard::Listener do
end
it "should be true when set" do
subject.instance_variable_get(:@watch_deletions).should eql true
subject.instance_variable_get(:@watch_all_modifications).should eql true
end
it "should track deleted files" do
@ -256,7 +256,7 @@ describe Guard::Listener do
describe "#ignore_paths" do
it "defaults to the default ignore paths" do
subject.new.ignore_paths.should == Guard::Listener::DefaultIgnorePaths
subject.new.ignore_paths.should == Guard::Listener::DEFAULT_IGNORE_PATHS
end
it "can be added to via :ignore_paths option" do