lib/guard/guardfile/evaluator.rb



require 'guard/options'

module Guard
  module Guardfile

    # This class is responsible for evaluating the Guardfile. It delegates
    # to Guard::Dsl for the actual objects generation from the Guardfile content.
    #
    # @see Guard::Dsl
    #
    class Evaluator

      attr_reader :options

      # Initializes a new Guard::Guardfile::Evaluator object.
      #
      # @option opts [String] guardfile the path to a valid Guardfile
      # @option opts [String] guardfile_contents a string representing the content of a valid Guardfile
      #
      def initialize(opts = {})
        @options = ::Guard::Options.new(opts.select { |k, _| [:guardfile, :guardfile_contents].include?(k.to_sym) })
      end

      # Evaluates the DSL methods in the `Guardfile`.
      #
      # @example Programmatically evaluate a Guardfile
      #   Guard::Guardfile::Evaluator.new.evaluate_guardfile
      #
      # @example Programmatically evaluate a Guardfile with a custom Guardfile path
      #   Guard::Guardfile::Evaluator.new(guardfile: '/Users/guardfile/MyAwesomeGuardfile').evaluate_guardfile
      #
      # @example Programmatically evaluate a Guardfile with an inline Guardfile
      #   Guard::Guardfile::Evaluator.new(guardfile_contents: 'guard :rspec').evaluate_guardfile
      #
      def evaluate_guardfile
        _fetch_guardfile_contents
        _instance_eval_guardfile(guardfile_contents)
      end

      # Re-evaluates the `Guardfile` to update
      # the current Guard configuration.
      #
      def reevaluate_guardfile
        _before_reevaluate_guardfile
        evaluate_guardfile
        _after_reevaluate_guardfile
      end

      # Tests if the current `Guardfile` contains a specific Guard plugin.
      #
      # @example Programmatically test if a Guardfile contains a specific Guard plugin
      #   File.read('Guardfile')
      #   #=> "guard :rspec"
      #
      #   Guard::Guardfile::Evaluator.new.guardfile_include?('rspec)
      #   #=> true
      #
      # @param [String] plugin_name the name of the Guard
      # @return [Boolean] whether the Guard plugin has been declared
      #
      def guardfile_include?(plugin_name)
        _guardfile_contents_without_user_config.match(/^guard\s*\(?\s*['":]#{ plugin_name }['"]?/)
      end

      # Gets the file path to the project `Guardfile`.
      #
      # @example Gets the path of the currently evaluated Guardfile
      #   Dir.pwd
      #   #=> "/Users/remy/Code/github/guard"
      #
      #   evaluator = Guard::Guardfile::Evaluator.new
      #   evaluator.evaluate_guardfile
      #   #=> nil
      #
      #   evaluator.guardfile_path
      #   #=> "/Users/remy/Code/github/guard/Guardfile"
      #
      # @example Gets the "path" of an inline Guardfile
      #   > Guard::Guardfile::Evaluator.new(guardfile_contents: 'guard :rspec').evaluate_guardfile
      #   => nil
      #
      #   > Guard::Guardfile::Evaluator.new.guardfile_path
      #   => "Inline Guardfile"
      #
      # @return [String] the path to the Guardfile or 'Inline Guardfile' if
      #   the Guardfile has been specified via the `:guardfile_contents` option.
      #
      def guardfile_path
        options[:guardfile_path] || ''
      end

      # Gets the content of the `Guardfile` concatenated with the global
      # user configuration file.
      #
      # @example Programmatically get the content of the current Guardfile
      #   Guard::Guardfile::Evaluator.new.guardfile_contents
      #   #=> "guard :rspec"
      #
      # @return [String] the Guardfile content
      #
      def guardfile_contents
        config = File.read(_user_config_path) if File.exist?(_user_config_path)
        [_guardfile_contents_without_user_config, config].compact.join("\n")
      end

      private

      # Gets the content of the `Guardfile`.
      #
      # @return [String] the Guardfile content
      #
      def _guardfile_contents_without_user_config
        options[:guardfile_contents] || ''
      end

      # Evaluates the content of the `Guardfile`.
      #
      # @param [String] contents the content to evaluate.
      #
      def _instance_eval_guardfile(contents)
        ::Guard::Dsl.new.instance_eval(contents, options[:guardfile_path], 1)
      rescue => ex
        ::Guard::UI.error "Invalid Guardfile, original error is:\n#{ $! }"
        raise ex
      end

      # Gets the content to evaluate and stores it into
      # the options as `:guardfile_contents`.
      #
      def _fetch_guardfile_contents
        _use_inline_guardfile || _use_provided_guardfile || _use_default_guardfile

        unless _guardfile_contents_usable?
          ::Guard::UI.error 'No Guard plugins found in Guardfile, please add at least one.'
        end
      end

      # Use the provided inline Guardfile if provided.
      #
      def _use_inline_guardfile
        return false unless options[:guardfile_contents]

        ::Guard::UI.info 'Using inline Guardfile.'
        options[:guardfile_path] = 'Inline Guardfile'
      end

      # Try to use the provided Guardfile. Exits Guard if the Guardfile cannot
      # be found.
      #
      def _use_provided_guardfile
        return false unless options[:guardfile]

        options[:guardfile] = File.expand_path(options[:guardfile])
        if File.exist?(options[:guardfile])
          _read_guardfile(options[:guardfile])
          ::Guard::UI.info "Using Guardfile at #{ options[:guardfile] }."
          true
        else
          ::Guard::UI.error "No Guardfile exists at #{ options[:guardfile] }."
          exit 1
        end
      end

      # Try to use one of the default Guardfiles (local or home Guardfile).
      # Exits Guard if no Guardfile is found.
      #
      def _use_default_guardfile
        if guardfile_path = _find_default_guardfile
          _read_guardfile(guardfile_path)
        else
          ::Guard::UI.error 'No Guardfile found, please create one with `guard init`.'
          exit 1
        end
      end

      # Returns the first default Guardfile (either local or home Guardfile)
      # or nil otherwise.
      #
      def _find_default_guardfile
        [_local_guardfile_path, _home_guardfile_path].find { |path| File.exist?(path) }
      end

      # Reads the current `Guardfile` content.
      #
      # @param [String] guardfile_path the path to the Guardfile
      #
      def _read_guardfile(guardfile_path)
        options[:guardfile_path]     = guardfile_path
        options[:guardfile_contents] = File.read(guardfile_path)
      rescue => ex
        ::Guard::UI.error ex.inspect
        ::Guard::UI.error("Error reading file #{ guardfile_path }")
        exit 1
      end

      # Stops Guard and clear internal state
      # before the Guardfile will be re-evaluated.
      #
      def _before_reevaluate_guardfile
        ::Guard.runner.run(:stop)
        ::Guard.reset_groups
        ::Guard.reset_plugins
        ::Guard.reset_scope
        ::Guard::Notifier.clear_notifiers
      end

      # Starts Guard and notification and show a message
      # after the Guardfile has been re-evaluated.
      #
      def _after_reevaluate_guardfile
        ::Guard::Notifier.turn_on if ::Guard::Notifier.enabled?

        if ::Guard.plugins.empty?
          ::Guard::Notifier.notify('No plugins found in Guardfile, please add at least one.', title: 'Guard re-evaluate', image: :failed)
        else
          msg = 'Guardfile has been re-evaluated.'
          ::Guard::UI.info(msg)
          ::Guard::Notifier.notify(msg, title: 'Guard re-evaluate')

          ::Guard.runner.run(:start)
        end
      end

      # Tests if the current `Guardfile` content is usable.
      #
      # @return [Boolean] if the Guardfile is usable
      #
      def _guardfile_contents_usable?
        guardfile_contents && guardfile_contents =~ /guard/m
      end

      # The path to the `Guardfile` that is located at
      # the directory, where Guard has been started from.
      #
      # @return [String] the path to the local Guardfile
      #
      def _local_guardfile_path
        File.expand_path(File.join(Dir.pwd, 'Guardfile'))
      end

      # The path to the `.Guardfile` that is located at
      # the users home directory.
      #
      # @return [String] the path to `~/.Guardfile`
      #
      def _home_guardfile_path
        File.expand_path(File.join('~', '.Guardfile'))
      end

      # The path to the user configuration `.guard.rb`
      # that is located at the users home directory.
      #
      # @return [String] the path to `~/.guard.rb`
      #
      def _user_config_path
        File.expand_path(File.join('~', '.guard.rb'))
      end

    end

  end
end