module Buildr # A filter knows how to copy a set of source files into a target directory, and apply # mapping to these files. # # You can specify the mapping using a Hash, and it will map ${key} fields found in each # source file into the appropriate value. For example: # filter.using "version"=>"1.2" # will replace all occurrences of "${version}" with "1.2". # # You can also specify the mapping by passing a proc or a method, that will be called for # each source file, with the file name and content, returning the modified content. # # Without any mapping, the filter simply copies the source files into the target directory. # # See Buildr#filter. class Filter # The target directory. attr_reader :target # The mapping. See #using. attr_accessor :mapping # The source files and directories. attr_accessor :sources def initialize() #:nodoc: @sources = FileList[] end # :call-seq: # include(*files) => self # # Specifies files to include and returns self. See FileList#include. def include(*files) @sources.include *files self end alias :add :include # :call-seq: # exclude(*files) => self # # Specifies files to exclude and returns self. See FileList#exclude. def exclude(*files) @sources.exclude *files self end # :call-seq: # into(dir) => self # # Specifies the target directory and return self. This tells the filter task where # to copy the source files to. # # For example: # filter.include("*.HTML").into("docs").run def into(dir) @target = File.expand_path(dir.to_s) self end # :call-seq: # using(mapping) => self # using() { |file_name, contents| ... } => self # # Specifies the mapping to use and returns self. # # The mapping can be a proc or a method called with the file name and content, returning # the modified content. Or the mapping can be a Hash for mapping each ${key} into a value. # Without any mapping, all files are copied as is. # # For example: # filter.using "version"=>"1.2" # will replace all occurrences of "${version}" with "1.2". def using(mapping, &block) self.mapping = mapping || block self end # Run the filter. def run() #@sources.each { |src| Rake.application[src, Rake.application.current_scope].invoke } if needed? fail "No target directory specified" if !target || (File.exist?(target.to_s) && !File.directory?(target.to_s)) unless copy_map.empty? verbose(Rake.application.options.trace || false) do mkpath target.to_s copy_map do |dest, src| mkpath File.dirname(dest) rescue nil case mapping when Proc, Method # Call on input, accept output. mapped = mapping.call(src, File.read(src)) File.open(dest, "w") { |file| file.write mapped } when Hash # Map ${key} to value mapped = File.read(src).gsub(/\$\{.*\}/) { |str| mapping[str[2..-2]] || str } File.open(dest, "w") { |file| file.write mapped } when nil # No mapping. cp src, dest else fail "Filter can be a hash (key=>value), or a proc/method; I don't understand #{mapping}" end end touch target.to_s end end end end # Returns the target directory. def to_s() @target.to_s end private def needed?() return false if target.nil? || copy_map.empty? return true unless File.exist?(target.to_s) return true if copy_map.any? { |dest, src| !File.exist?(dest) || File.mtime(src) > File.mtime(dest) } false end # Return a copy map of all the files that need copying: the key is the file to copy to, # the value is the source file. If called with a block, yields with each dest/source pair. def copy_map(&block) # Create a map between the source file and the similarly named file in the target directory, # including all files nested inside directories. @copy_map ||= @sources.map(&:to_s).inject({}) do |map, path| if File.directory?(path) Dir["#{path}/**/*"].each do |file| map[file.sub(File.dirname(path), target.to_s)] = file unless File.directory?(file) || @sources.exclude?(file) end elsif File.exist?(path) map[File.join(target.to_s, File.basename(path))] = path end map end.reject do |dest, src| # ... while ignoring that which does not need updating. File.exist?(dest) && File.stat(dest).mtime > File.stat(src).mtime end if block_given? @copy_map.each(&block) else @copy_map end end end # :call-seq: # filter(*files) => Filter # # Creates a filter task to operate on all the specified files. def filter(*files) Filter.new.include *files end end