# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF # licenses this file to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. module Buildr # A filter knows how to copy files from one directory to another, applying mappings to the # contents of 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 in the target file. For example: # # filter.using 'version'=>'1.2', 'build'=>Time.now # # will replace all occurrences of ${version} with 1.2, and ${build} # with the current date/time. # # 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 files from the source directory into the target # directory. # # A filter has one target directory, but you can specify any number of source directories, # either when creating the filter or calling #from. Include/exclude patterns are specified # relative to the source directories, so: # filter.include '*.png' # will only include PNG files from any of the source directories. # # See Buildr#filter. class Filter def initialize #:nodoc: clear end # Returns the list of source directories (each being a file task). attr_reader :sources # :call-seq: # clear => self # # Clear filter sources and include/exclude patterns def clear @include = [] @exclude = [] @sources = [] self end # :call-seq: # from(*sources) => self # # Adds additional directories from which to copy resources. # # For example: # filter.from('src').into('target').using('build'=>Time.now) def from(*sources) @sources |= sources.flatten.map { |dir| file(File.expand_path(dir.to_s)) } self end # The target directory as a file task. attr_reader :target # :call-seq: # into(dir) => self # # Sets the target directory into which files are copied and returns self. # # For example: # filter.from('src').into('target').using('build'=>Time.now) def into(dir) @target = file(File.expand_path(dir.to_s)) { |task| run if target == task } self end # :call-seq: # include(*files) => self # # Specifies files to include and returns self. See FileList#include. # # By default all files are included. You can use this method to only include specific # files form the source directory. def include(*files) @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) @exclude += files self end # The mapping. See #using. attr_accessor :mapping # The mapper to use. See #using. attr_accessor :mapper # :call-seq: # using(mapping) => self # using { |file_name, contents| ... } => self # # Specifies the mapping to use and returns self. # # The most typical mapping uses a Hash, and the default mapping uses the Maven style, so # ${key} are mapped to the values. You can change that by passing a different # format as the first argument. Currently supports: # * :ant -- Map @key@. # * :maven -- Map ${key} (default). # * :ruby -- Map #{key}. # * Regexp -- Maps the matched data (e.g. /=(.*?)=/ # # For example: # filter.using 'version'=>'1.2' # Is the same as: # filter.using :maven, 'version'=>'1.2' # # You can also pass a proc or method. It will be called with the file name and content, # to return the mapped content. # # Without any mapping, all files are copied as is. def using(*args, &block) case args.first when Hash # Maven hash mapping using :maven, *args when Symbol # Mapping from a method raise ArgumentError, 'Expected mapper type followed by mapping hash' unless args.size == 2 && Hash === args[1] @mapper, @mapping = *args when Regexp # Mapping using a regular expression raise ArgumentError, 'Expected regular expression followed by mapping hash' unless args.size == 2 && Hash === args[1] @mapper, @mapping = *args else raise ArgumentError, 'Expected proc, method or a block' if args.size > 1 || (args.first && block) @mapping = args.first || block end self end # :call-seq: # run => boolean # # Runs the filter. def run sources.each { |source| raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s) } raise 'No target directory specified, where am I going to copy the files to?' if target.nil? copy_map = sources.flatten.map(&:to_s).inject({}) do |map, source| files = Util.recursive_with_dot_files(source). map { |file| Util.relative_path(file, source) }. select { |file| @include.empty? || @include.any? { |pattern| File.fnmatch(pattern, file, File::FNM_PATHNAME) } }. reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file, File::FNM_PATHNAME) } } files.each do |file| src, dest = File.expand_path(file, source), File.expand_path(file, target.to_s) map[file] = src if !File.exist?(dest) || File.stat(src).mtime > File.stat(dest).mtime end map end mkpath target.to_s return false if copy_map.empty? verbose(Buildr.application.options.trace || false) do copy_map.each do |path, source| dest = File.expand_path(path, target.to_s) if File.directory?(source) mkpath dest else mkpath File.dirname(dest) case mapping when Proc, Method # Call on input, accept output. mapped = mapping.call(path, File.open(source, 'rb') { |file| file.read }) File.open(dest, 'wb') { |file| file.write mapped } when Hash # Map ${key} to value content = File.open(source, 'rb') { |file| file.read } if Symbol === @mapper mapped = send("#{@mapper}_mapper", content) { |key| mapping[key] } else mapped = regexp_mapper(content) { |key| mapping[key] } end #gsub(/\$\{[^}]*\}/) { |str| mapping[str[2..-2]] || str } File.open(dest, 'wb') { |file| file.write mapped } when nil # No mapping. cp source, dest File.chmod(0664, dest) else fail "Filter can be a hash (key=>value), or a proc/method; I don't understand #{mapping}" end end end touch target.to_s end true end # Returns the target directory. def to_s @target.to_s end private def maven_mapper(content) content.gsub(/\$\{.*?\}/) { |str| yield(str[2..-2]) || str } end def ant_mapper(content) content.gsub(/@.*?@/) { |str| yield(str[1..-2]) || str } end def ruby_mapper(content) content.gsub(/#\{.*?\}/) { |str| yield(str[2..-2]) || str } end def regexp_mapper(content) content.gsub(@mapper) { |str| yield(str.scan(@mapper).join) || str } end end # :call-seq: # filter(*source) => Filter # # Creates a filter that will copy files from the source directory(ies) into the target directory. # You can extend the filter to modify files by mapping ${key} into values in each # of the copied files, and by including or excluding specific files. # # A filter is not a task, you must call the Filter#run method to execute it. # # For example, to copy all files from one directory to another: # filter('src/files').into('target/classes').run # To include only the text files, and replace each instance of ${build} with the current # date/time: # filter('src/files').into('target/classes').include('*.txt').using('build'=>Time.now).run def filter(*sources) Filter.new.from(*sources) end end