diff --git a/lib/guard/dsl.rb b/lib/guard/dsl.rb index fdf736b..d8bcabc 100644 --- a/lib/guard/dsl.rb +++ b/lib/guard/dsl.rb @@ -1,62 +1,158 @@ 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 - def evaluate_guardfile(options = {}) - options.is_a?(Hash) or raise ArgumentError.new("evaluate_guardfile not passed a Hash!") + # Evaluate the DSL methods in the `Guardfile`. + # + # @param [Hash] options the Guard options + # @raise [ArgumentError] when options are not a Hash + # + def evaluate_guardfile(options = { }) + 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. + # + # @return [String] the content of the Guardfile. + # 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,57 +160,115 @@ 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 + # 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 [String] name the group's name called from the CLI + # @yield a block where you can declare several guards + # + # @see Dsl#guard + # def group(name, &guard_definition) @groups = @@options[:group] || [] - name = name.to_sym + name = name.to_sym if guard_definition && (@groups.empty? || @groups.map(&:to_sym).include?(name)) @current_group = name @@ -123,25 +277,71 @@ module Guard 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 + # + 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. + # + # @params [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 + # + # @params [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