diff --git a/.gitignore b/.gitignore index 36ec22f..953ae0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ pkg/* +doc/* *.gem *.rbc .*.swp *.bak .bundle +.yardoc Gemfile.lock ## MAC OS @@ -12,4 +14,4 @@ Gemfile.lock .com.apple.timemachine.supported .fseventsd Desktop DB -Desktop DF \ No newline at end of file +Desktop DF diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..9276dfc --- /dev/null +++ b/.yardopts @@ -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 diff --git a/Gemfile b/Gemfile index 5aa0145..36c7f02 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/guard.gemspec b/guard.gemspec index 0cd3e1b..258b7a2 100644 --- a/guard.gemspec +++ b/guard.gemspec @@ -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' diff --git a/lib/guard.rb b/lib/guard.rb index 037f5d8..53c88ec 100644 --- a/lib/guard.rb +++ b/lib/guard.rb @@ -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] 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] 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) @@ -39,48 +62,58 @@ module Guard listener.changed_files += files if Watcher.match_files?(guards, files) end - UI.info "Guard is now watching at '#{listener.directory}'" + UI.info "Guard is now watching at '#{ listener.directory }'" guards.each { |guard| supervised_task(guard, :start) } interactor.start 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) + guard.hook("#{ task_to_supervise }_begin", *args) result = guard.send(task_to_supervise, *args) - guard.hook("#{task_to_supervise}_end", result) + 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")}") + 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 + UI.info("\n#{ guard.class.name } has just been fired") + + ex end + # Add a Guard to use. + # + # @param [String] name the Guard name + # @param [Array] watchers the list of declared watchers + # @param [Array] 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,42 +191,58 @@ 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 const_name = name.downcase.gsub('-', '') begin - require "guard/#{name.downcase}" if try_require + require "guard/#{ name.downcase }" if try_require self.const_get(self.constants.find { |c| c.to_s.downcase == const_name }) rescue TypeError unless try_require try_require = true retry else - UI.error "Could not find class Guard::#{const_name.capitalize}" + UI.error "Could not find class Guard::#{ const_name.capitalize }" end rescue LoadError => loadError - UI.error "Could not load 'guard/#{name.downcase}' or find class Guard::#{const_name.capitalize}" + UI.error "Could not load 'guard/#{ name.downcase }' or find class Guard::#{ const_name.capitalize }" UI.error loadError.to_s 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 + Gem::Specification.find_by_name("guard-#{ name }").full_gem_path else - Gem.source_index.find_name("guard-#{name}").last.full_gem_path + Gem.source_index.find_name("guard-#{ name }").last.full_gem_path end rescue - UI.error "Could not find 'guard-#{name}' gem path." + UI.error "Could not find 'guard-#{ name }' gem path." end - ## # Returns a list of guard Gem names installed locally. + # + # @return [Array] 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,16 +251,19 @@ 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| - ::Guard::UI.debug "Command execution: #{command} #{args.join(' ')}" + ::Guard::UI.debug "Command execution: #{ command } #{ args.join(' ') }" original_system command, *args end - Kernel.send(:alias_method, :original_backtick, :"`") - Kernel.send(:define_method, :"`") do |command| - ::Guard::UI.debug "Command execution: #{command}" + Kernel.send(:alias_method, :original_backtick, :'`') + Kernel.send(:define_method, :'`') do |command| + ::Guard::UI.debug "Command execution: #{ command }" original_backtick command end end diff --git a/lib/guard/cli.rb b/lib/guard/cli.rb index 7c8c761..aed0e2b 100644 --- a/lib/guard/cli.rb +++ b/lib/guard/cli.rb @@ -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 + Guard::UI.info 'Available guards:' + + Guard::guard_gem_names.sort.uniq.each do |name| + Guard::UI.info " #{ name } #{ installed.include?(name) ? '*' : '' }" 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 ' ' + 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", "Prints Guard's version" - def version - ::Guard::UI.info "Guard version #{Guard::VERSION}" - 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 - end + # 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) + + else + 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" - def show - ::Guard::DslDescriber.evaluate_guardfile(options) + desc 'show', 'Show all defined Guards and their options' + map %w(-T) => :show - ::Guard::DslDescriber.guardfile_structure.each do |group| - if !group[:guards].empty? + # 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]}:" + Guard::UI.info "Group #{ group[:group] }:" else - ::Guard::UI.info "(global):" + Guard::UI.info '(global):' end group[:guards].each do |guard| - line = " #{guard[:name]}" + 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 diff --git a/lib/guard/dsl.rb b/lib/guard/dsl.rb index fdf736b..f8d0fc3 100644 --- a/lib/guard/dsl.rb +++ b/lib/guard/dsl.rb @@ -1,62 +1,160 @@ 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] 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 + new.instance_eval(contents, @@options[:guardfile_path], 1) + rescue + UI.error "Invalid Guardfile, original error is:\n#{ $! }" + exit 1 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}['"]?/) + 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 + @@options[:guardfile_path] = guardfile_path + @@options[:guardfile_contents] = File.read(guardfile_path) + rescue + UI.error("Error reading file #{ guardfile_path }") + exit 1 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] if File.exist?(@@options[:guardfile]) read_guardfile(@@options[:guardfile]) - UI.info "Using Guardfile at #{@@options[:guardfile]}." + UI.info "Using Guardfile at #{ @@options[:guardfile] }." else - UI.error "No Guardfile exists at #{@@options[:guardfile]}." + UI.error "No Guardfile exists at #{ @@options[:guardfile] }." exit 1 end @@ -64,84 +162,192 @@ 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 unless guardfile_contents_usable? - UI.error "The command file(#{@@options[:guardfile]}) seems to be empty." + UI.error "The command file(#{ @@options[:guardfile] }) seems to be empty." exit 1 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 + 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 + 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(', ')}" + UI.info "Ignoring paths: #{ paths.join(', ') }" ::Guard.listener.ignore_paths.push(*paths) end end diff --git a/lib/guard/dsl_describer.rb b/lib/guard/dsl_describer.rb index b9908ff..be2446e 100644 --- a/lib/guard/dsl_describer.rb +++ b/lib/guard/dsl_describer.rb @@ -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] 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 diff --git a/lib/guard/guard.rb b/lib/guard/guard.rb index b1c4491..252e67d 100644 --- a/lib/guard/guard.rb +++ b/lib/guard/guard.rb @@ -1,21 +1,39 @@ 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] 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" + ::Guard::UI.info "Guardfile already includes #{ name } guard" else content = File.read('Guardfile') - guard = File.read("#{::Guard.locate_guard(name)}/lib/guard/#{name}/templates/Guardfile") + guard = File.read("#{ ::Guard.locate_guard(name) }/lib/guard/#{ name }/templates/Guardfile") File.open('Guardfile', 'wb') do |f| f.puts(content) f.puts("") @@ -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] paths the changes files or paths + # @return [Boolean] Whether the run_all action was successful or not + # def run_on_change(paths) true end diff --git a/lib/guard/hook.rb b/lib/guard/hook.rb index 3f57b30..a75f4c1 100644 --- a/lib/guard/hook.rb +++ b/lib/guard/hook.rb @@ -1,51 +1,80 @@ 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] - "#{calling_method}_#{event}" - else - event - end.to_sym + calling_method = caller[0][/`([^']*)'/, 1] + "#{ calling_method }_#{ event }" + else + event + end.to_sym - UI.debug "Hook :#{hook_name} executed for #{self.class}" + UI.debug "Hook :#{ hook_name } executed for #{ self.class }" Hook.notify(self.class, hook_name, *args) end 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] 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 diff --git a/lib/guard/interactor.rb b/lib/guard/interactor.rb index 5c5fd09..1ef94ca 100644 --- a/lib/guard/interactor.rb +++ b/lib/guard/interactor.rb @@ -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 diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb index e3f8572..49cd083 100644 --- a/lib/guard/listener.rb +++ b/lib/guard/listener.rb @@ -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] 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 |= options[:ignore_paths] if options[:ignore_paths] + @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 - def modified_files(dirs, options={}) + # Get the modified files. + # + # @param [Array] 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,46 +147,70 @@ 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] 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] paths the paths to change + # @return [Array] 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] dirs the directory to listen to + # @param [Array] 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)) end end - private + private - def potentially_modified_files(dirs, options={}) + # Gets a list of files that are in the modified firectories. + # + # @param [Array] 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) if options[:all] @@ -150,7 +218,7 @@ module Guard if File.file?(path) array << path else - array += Dir.glob("#{path}/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) } + array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) } end array end @@ -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 diff --git a/lib/guard/listeners/darwin.rb b/lib/guard/listeners/darwin.rb index 07fafc6..420680b 100644 --- a/lib/guard/listeners/darwin.rb +++ b/lib/guard/listeners/darwin.rb @@ -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 + 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) diff --git a/lib/guard/listeners/linux.rb b/lib/guard/listeners/linux.rb index 68453cb..ba31076 100644 --- a/lib/guard/listeners/linux.rb +++ b/lib/guard/listeners/linux.rb @@ -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 + 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 diff --git a/lib/guard/listeners/polling.rb b/lib/guard/listeners/polling.rb index 894bff9..ffc6508 100644 --- a/lib/guard/listeners/polling.rb +++ b/lib/guard/listeners/polling.rb @@ -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 - private + # 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 diff --git a/lib/guard/listeners/windows.rb b/lib/guard/listeners/windows.rb index 1f74d14..f7be41e 100644 --- a/lib/guard/listeners/windows.rb +++ b/lib/guard/listeners/windows.rb @@ -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 + private + # 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 diff --git a/lib/guard/notifier.rb b/lib/guard/notifier.rb index e9ea811..e9b7f8f 100644 --- a/lib/guard/notifier.rb +++ b/lib/guard/notifier.rb @@ -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 diff --git a/lib/guard/ui.rb b/lib/guard/ui.rb index a52b42a..eb543a3 100644 --- a/lib/guard/ui.rb +++ b/lib/guard/ui.rb @@ -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 - def info(message, options = {}) - unless ENV["GUARD_ENV"] == "test" + # 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' reset_line if options[:reset] puts color(message) if message != '' end end - def error(message, options={}) - unless ENV["GUARD_ENV"] == "test" + # 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' reset_line if options[:reset] puts color('ERROR: ', :red) + message end end - def deprecation(message, options = {}) - unless ENV["GUARD_ENV"] == "test" + # 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' reset_line if options[:reset] puts color('DEPRECATION: ', :red) + message end end - def debug(message, options={}) - unless ENV["GUARD_ENV"] == "test" + # 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' 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 + 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 diff --git a/lib/guard/version.rb b/lib/guard/version.rb index 9e7bef1..a7693dc 100644 --- a/lib/guard/version.rb +++ b/lib/guard/version.rb @@ -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 diff --git a/lib/guard/watcher.rb b/lib/guard/watcher.rb index 7955c5d..5a4071b 100644 --- a/lib/guard/watcher.rb +++ b/lib/guard/watcher.rb @@ -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" + + 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] files the changed files + # @return [Array] 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] guards the guards to use the watchers from + # @param [Array] 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,15 +84,25 @@ module Guard end end + # Test if any of the files is the Guardfile. + # + # @param [Array] 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 } + 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 rescue Exception => e - UI.error "Problem with watch action!\n#{e.message}\n\n#{e.backtrace.join("\n")}" + UI.error "Problem with watch action!\n#{ e.message }\n\n#{ e.backtrace.join("\n") }" end end diff --git a/spec/guard/dsl_spec.rb b/spec/guard/dsl_spec.rb index 2050ee6..965cf58 100644 --- a/spec/guard/dsl_spec.rb +++ b/spec/guard/dsl_spec.rb @@ -220,16 +220,16 @@ describe Guard::Dsl do describe "#ignore_paths" do disable_user_config - + it "adds the paths to the listener's ignore_paths" do ::Guard.stub!(:listener).and_return(mock('Listener')) ::Guard.listener.should_receive(:ignore_paths).and_return(ignore_paths = ['faz']) - + subject.evaluate_guardfile(:guardfile_contents => "ignore_paths 'foo', 'bar'") ignore_paths.should == ['faz', 'foo', 'bar'] end end - + describe "#group" do disable_user_config diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb index 6a2c536..6819f7b 100644 --- a/spec/guard/listener_spec.rb +++ b/spec/guard/listener_spec.rb @@ -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