lib/guard/copy.rb
require 'guard'
require 'guard/plugin'
require 'fileutils'
module Guard
class Copy < Plugin
autoload :Target, 'guard/copy/target'
attr_reader :targets
# Initializes a Guard plugin.
# Don't do any work here, especially as Guard plugins get initialized even
# if they are not in an active group!
#
# @param [Hash] options the Guard plugin options
# @option options [Array<Guard::Watcher>] watchers the Guard plugin file
# watchers
# @option options [Symbol] group the group this Guard plugin belongs to
# @option options [Boolean] any_return allow any object to be returned from
# a watcher
#
def initialize(options = {})
inject_watchers(options)
super
@targets = Array(options[:to]).map { |to| Target.new(to, options) }
end
# Call once when Guard starts. Please override initialize method to init stuff.
# @raise [:task_has_failed] when start has failed
def start
validate_presence_of(:from)
validate_from_is_directory
validate_presence_of(:to)
validate_to_patterns_are_not_absolute
validate_to_does_not_start_with_from
resolve_targets!
validate_no_targets_are_files
display_target_paths
run_all if options[:run_at_start]
end
# Called when just `enter` is pressed
# This method should be principally used for long action like running all specs/tests/...
# @raise [:task_has_failed] when run_all has failed
def run_all
run_on_changes(Watcher.match_files(self, Dir.glob("**/*")))
end
# Called on file(s) modifications that the Guard watches.
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_changes has failed
def run_on_changes(paths)
validate_at_least_one_target('copy')
with_all_target_paths(paths) do |from_path, to_path|
to_dir = File.dirname(to_path)
if !File.directory?(to_dir) && options[:mkpath]
UI.info("creating directory #{to_dir}") if options[:verbose]
FileUtils.mkpath(to_dir)
end
validate_to_path(to_path)
UI.info("copying to #{to_path}") if options[:verbose]
copy(from_path, to_path)
end
end
# Called on file(s) deletions that the Guard watches.
# @param [Array<String>] paths the deleted files or paths
# @raise [:task_has_failed] when run_on_removals has failed
def run_on_removals(paths)
return unless options[:delete]
validate_at_least_one_target('delete')
with_all_target_paths(paths) do |_, to_path|
validate_to_file(to_path)
UI.info("deleting #{to_path}") if options[:verbose]
FileUtils.rm(to_path)
end
end
private
def inject_watchers(options)
if !options[:watchers] || options[:watchers].empty?
options[:watchers] = [::Guard::Watcher.new(%r{^#{options[:from]}/.*$})]
else
options[:watchers].each { |w| normalize_watcher(w, options[:from]) }
end
end
def target_paths
@targets.map { |t| t.paths }.flatten
end
def with_all_target_paths(paths)
paths.each do |from_path|
target_paths.each do |target_path|
to_path = from_path.sub(@options[:from], target_path)
yield(from_path, to_path)
end
end
end
def normalize_watcher(watcher, from)
unless watcher.pattern.source =~ %r{^\^#{from}/.*}
normalized_source = watcher.pattern.source.sub(%r{^\^?(#{from})?/?}, "^#{from}/")
UI.info('Guard::Copy is changing watcher pattern:')
UI.info(" #{watcher.pattern.source}")
UI.info('to:')
UI.info(" #{normalized_source}")
watcher.pattern = Regexp.new(normalized_source)
end
end
def validate_presence_of(option)
unless options[option]
UI.error("Guard::Copy - :#{option} option is required")
throw :task_has_failed
end
end
def validate_from_is_directory
path = options[:from]
unless File.directory?(path)
if File.file?(path)
UI.error("Guard::Copy - '#{path}' is a file and must be a directory")
throw :task_has_failed
else
UI.error("Guard::Copy - :from option does not contain a valid directory")
throw :task_has_failed
end
end
end
def validate_to_does_not_start_with_from
if Array(options[:to]).any? { |to| to.start_with?(options[:from]) }
UI.error('Guard::Copy - :to must not start with :from')
throw :task_has_failed
end
end
def validate_at_least_one_target(operation)
if target_paths.empty?
UI.error("Guard::Copy - cannot #{operation}, no valid :to directories")
throw :task_has_failed
end
end
def validate_to_path(to_path)
to_dir = File.dirname(to_path)
unless File.directory?(to_dir)
UI.error('Guard::Copy - cannot copy, directory path does not exist:')
UI.error(" #{to_dir}")
throw :task_has_failed
end
end
def validate_to_file(to_file)
unless File.file?(to_file)
UI.error('Guard::Copy - cannot delete, file does not exist:')
UI.error(" #{to_file}")
throw :task_has_failed
end
end
def resolve_targets!
@targets.each { |target| target.resolve! }
end
def validate_no_targets_are_files
target_paths.each do |path|
if File.file?(path)
UI.error('Guard::Copy - :to option contains a file and must be all directories')
throw :task_has_failed
end
end
end
def validate_to_patterns_are_not_absolute
targets.each do |target|
if target.absolute? && !options[:absolute]
UI.error('Guard::Copy - :to contains an absolute path:')
UI.error(" #{target.pattern}")
UI.error('Set the :absolute option to allow absolute target paths')
throw :task_has_failed
end
end
end
def display_target_paths
if target_paths.any?
UI.info("Guard::Copy - files in:")
UI.info(" #{options[:from]}")
UI.info("will be copied to#{ ' and removed from' if options[:delete] }:")
target_paths.each { |target_path| UI.info(" #{target_path}") }
end
end
def copy(from_path, to_path)
begin
FileUtils.cp(from_path, to_path)
rescue Errno::EISDIR
UI.warning("matched path is a directory; skipping")
UI.warning(" #{ from_path }")
end
end
end
end