1require 'rake/cloneable'
2require 'rake/file_utils_ext'
3require 'rake/pathmap'
4
5######################################################################
6module Rake
7
8  # #########################################################################
9  # A FileList is essentially an array with a few helper methods defined to
10  # make file manipulation a bit easier.
11  #
12  # FileLists are lazy.  When given a list of glob patterns for possible files
13  # to be included in the file list, instead of searching the file structures
14  # to find the files, a FileList holds the pattern for latter use.
15  #
16  # This allows us to define a number of FileList to match any number of
17  # files, but only search out the actual files when then FileList itself is
18  # actually used.  The key is that the first time an element of the
19  # FileList/Array is requested, the pending patterns are resolved into a real
20  # list of file names.
21  #
22  class FileList
23
24    include Cloneable
25
26    # == Method Delegation
27    #
28    # The lazy evaluation magic of FileLists happens by implementing all the
29    # array specific methods to call +resolve+ before delegating the heavy
30    # lifting to an embedded array object (@items).
31    #
32    # In addition, there are two kinds of delegation calls.  The regular kind
33    # delegates to the @items array and returns the result directly.  Well,
34    # almost directly.  It checks if the returned value is the @items object
35    # itself, and if so will return the FileList object instead.
36    #
37    # The second kind of delegation call is used in methods that normally
38    # return a new Array object.  We want to capture the return value of these
39    # methods and wrap them in a new FileList object.  We enumerate these
40    # methods in the +SPECIAL_RETURN+ list below.
41
42    # List of array methods (that are not in +Object+) that need to be
43    # delegated.
44    ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
45
46    # List of additional methods that must be delegated.
47    MUST_DEFINE = %w[to_a inspect <=>]
48
49    # List of methods that should not be delegated here (we define special
50    # versions of them explicitly below).
51    MUST_NOT_DEFINE = %w[to_a to_ary partition *]
52
53    # List of delegated methods that return new array values which need
54    # wrapping.
55    SPECIAL_RETURN = %w[
56      map collect sort sort_by select find_all reject grep
57      compact flatten uniq values_at
58      + - & |
59    ]
60
61    DELEGATING_METHODS = (ARRAY_METHODS + MUST_DEFINE - MUST_NOT_DEFINE).collect{ |s| s.to_s }.sort.uniq
62
63    # Now do the delegation.
64    DELEGATING_METHODS.each_with_index do |sym, i|
65      if SPECIAL_RETURN.include?(sym)
66        ln = __LINE__+1
67        class_eval %{
68          def #{sym}(*args, &block)
69            resolve
70            result = @items.send(:#{sym}, *args, &block)
71            FileList.new.import(result)
72          end
73        }, __FILE__, ln
74      else
75        ln = __LINE__+1
76        class_eval %{
77          def #{sym}(*args, &block)
78            resolve
79            result = @items.send(:#{sym}, *args, &block)
80            result.object_id == @items.object_id ? self : result
81          end
82        }, __FILE__, ln
83      end
84    end
85
86    # Create a file list from the globbable patterns given.  If you wish to
87    # perform multiple includes or excludes at object build time, use the
88    # "yield self" pattern.
89    #
90    # Example:
91    #   file_list = FileList.new('lib/**/*.rb', 'test/test*.rb')
92    #
93    #   pkg_files = FileList.new('lib/**/*') do |fl|
94    #     fl.exclude(/\bCVS\b/)
95    #   end
96    #
97    def initialize(*patterns)
98      @pending_add = []
99      @pending = false
100      @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup
101      @exclude_procs = DEFAULT_IGNORE_PROCS.dup
102      @items = []
103      patterns.each { |pattern| include(pattern) }
104      yield self if block_given?
105    end
106
107    # Add file names defined by glob patterns to the file list.  If an array
108    # is given, add each element of the array.
109    #
110    # Example:
111    #   file_list.include("*.java", "*.cfg")
112    #   file_list.include %w( math.c lib.h *.o )
113    #
114    def include(*filenames)
115      # TODO: check for pending
116      filenames.each do |fn|
117        if fn.respond_to? :to_ary
118          include(*fn.to_ary)
119        else
120          @pending_add << fn
121        end
122      end
123      @pending = true
124      self
125    end
126    alias :add :include
127
128    # Register a list of file name patterns that should be excluded from the
129    # list.  Patterns may be regular expressions, glob patterns or regular
130    # strings.  In addition, a block given to exclude will remove entries that
131    # return true when given to the block.
132    #
133    # Note that glob patterns are expanded against the file system. If a file
134    # is explicitly added to a file list, but does not exist in the file
135    # system, then an glob pattern in the exclude list will not exclude the
136    # file.
137    #
138    # Examples:
139    #   FileList['a.c', 'b.c'].exclude("a.c") => ['b.c']
140    #   FileList['a.c', 'b.c'].exclude(/^a/)  => ['b.c']
141    #
142    # If "a.c" is a file, then ...
143    #   FileList['a.c', 'b.c'].exclude("a.*") => ['b.c']
144    #
145    # If "a.c" is not a file, then ...
146    #   FileList['a.c', 'b.c'].exclude("a.*") => ['a.c', 'b.c']
147    #
148    def exclude(*patterns, &block)
149      patterns.each do |pat|
150        @exclude_patterns << pat
151      end
152      if block_given?
153        @exclude_procs << block
154      end
155      resolve_exclude if ! @pending
156      self
157    end
158
159
160    # Clear all the exclude patterns so that we exclude nothing.
161    def clear_exclude
162      @exclude_patterns = []
163      @exclude_procs = []
164      self
165    end
166
167    # Define equality.
168    def ==(array)
169      to_ary == array
170    end
171
172    # Return the internal array object.
173    def to_a
174      resolve
175      @items
176    end
177
178    # Return the internal array object.
179    def to_ary
180      to_a
181    end
182
183    # Lie about our class.
184    def is_a?(klass)
185      klass == Array || super(klass)
186    end
187    alias kind_of? is_a?
188
189    # Redefine * to return either a string or a new file list.
190    def *(other)
191      result = @items * other
192      case result
193      when Array
194        FileList.new.import(result)
195      else
196        result
197      end
198    end
199
200    # Resolve all the pending adds now.
201    def resolve
202      if @pending
203        @pending = false
204        @pending_add.each do |fn| resolve_add(fn) end
205        @pending_add = []
206        resolve_exclude
207      end
208      self
209    end
210
211    def resolve_add(fn)
212      case fn
213      when %r{[*?\[\{]}
214        add_matching(fn)
215      else
216        self << fn
217      end
218    end
219    private :resolve_add
220
221    def resolve_exclude
222      reject! { |fn| exclude?(fn) }
223      self
224    end
225    private :resolve_exclude
226
227    # Return a new FileList with the results of running +sub+ against each
228    # element of the original list.
229    #
230    # Example:
231    #   FileList['a.c', 'b.c'].sub(/\.c$/, '.o')  => ['a.o', 'b.o']
232    #
233    def sub(pat, rep)
234      inject(FileList.new) { |res, fn| res << fn.sub(pat,rep) }
235    end
236
237    # Return a new FileList with the results of running +gsub+ against each
238    # element of the original list.
239    #
240    # Example:
241    #   FileList['lib/test/file', 'x/y'].gsub(/\//, "\\")
242    #      => ['lib\\test\\file', 'x\\y']
243    #
244    def gsub(pat, rep)
245      inject(FileList.new) { |res, fn| res << fn.gsub(pat,rep) }
246    end
247
248    # Same as +sub+ except that the original file list is modified.
249    def sub!(pat, rep)
250      each_with_index { |fn, i| self[i] = fn.sub(pat,rep) }
251      self
252    end
253
254    # Same as +gsub+ except that the original file list is modified.
255    def gsub!(pat, rep)
256      each_with_index { |fn, i| self[i] = fn.gsub(pat,rep) }
257      self
258    end
259
260    # Apply the pathmap spec to each of the included file names, returning a
261    # new file list with the modified paths.  (See String#pathmap for
262    # details.)
263    def pathmap(spec=nil)
264      collect { |fn| fn.pathmap(spec) }
265    end
266
267    # Return a new FileList with <tt>String#ext</tt> method applied to
268    # each member of the array.
269    #
270    # This method is a shortcut for:
271    #
272    #    array.collect { |item| item.ext(newext) }
273    #
274    # +ext+ is a user added method for the Array class.
275    def ext(newext='')
276      collect { |fn| fn.ext(newext) }
277    end
278
279
280    # Grep each of the files in the filelist using the given pattern. If a
281    # block is given, call the block on each matching line, passing the file
282    # name, line number, and the matching line of text.  If no block is given,
283    # a standard emacs style file:linenumber:line message will be printed to
284    # standard out.  Returns the number of matched items.
285    def egrep(pattern, *options)
286      matched = 0
287      each do |fn|
288        begin
289          open(fn, "r", *options) do |inf|
290            count = 0
291            inf.each do |line|
292              count += 1
293              if pattern.match(line)
294                matched += 1
295                if block_given?
296                  yield fn, count, line
297                else
298                  puts "#{fn}:#{count}:#{line}"
299                end
300              end
301            end
302          end
303        rescue StandardError => ex
304          $stderr.puts "Error while processing '#{fn}': #{ex}"
305        end
306      end
307      matched
308    end
309
310    # Return a new file list that only contains file names from the current
311    # file list that exist on the file system.
312    def existing
313      select { |fn| File.exist?(fn) }
314    end
315
316    # Modify the current file list so that it contains only file name that
317    # exist on the file system.
318    def existing!
319      resolve
320      @items = @items.select { |fn| File.exist?(fn) }
321      self
322    end
323
324    # FileList version of partition.  Needed because the nested arrays should
325    # be FileLists in this version.
326    def partition(&block)       # :nodoc:
327      resolve
328      result = @items.partition(&block)
329      [
330        FileList.new.import(result[0]),
331        FileList.new.import(result[1]),
332      ]
333    end
334
335    # Convert a FileList to a string by joining all elements with a space.
336    def to_s
337      resolve
338      self.join(' ')
339    end
340
341    # Add matching glob patterns.
342    def add_matching(pattern)
343      FileList.glob(pattern).each do |fn|
344        self << fn unless exclude?(fn)
345      end
346    end
347    private :add_matching
348
349    # Should the given file name be excluded?
350    def exclude?(fn)
351      return true if @exclude_patterns.any? do |pat|
352        case pat
353        when Regexp
354          fn =~ pat
355        when /[*?]/
356          File.fnmatch?(pat, fn, File::FNM_PATHNAME)
357        else
358          fn == pat
359        end
360      end
361      @exclude_procs.any? { |p| p.call(fn) }
362    end
363
364    DEFAULT_IGNORE_PATTERNS = [
365      /(^|[\/\\])CVS([\/\\]|$)/,
366      /(^|[\/\\])\.svn([\/\\]|$)/,
367      /\.bak$/,
368      /~$/
369    ]
370    DEFAULT_IGNORE_PROCS = [
371      proc { |fn| fn =~ /(^|[\/\\])core$/ && ! File.directory?(fn) }
372    ]
373
374    def import(array)
375      @items = array
376      self
377    end
378
379    class << self
380      # Create a new file list including the files listed. Similar to:
381      #
382      #   FileList.new(*args)
383      def [](*args)
384        new(*args)
385      end
386
387      # Get a sorted list of files matching the pattern. This method
388      # should be prefered to Dir[pattern] and Dir.glob(pattern) because
389      # the files returned are guaranteed to be sorted.
390      def glob(pattern, *args)
391        Dir.glob(pattern, *args).sort
392      end
393    end
394  end
395end
396
397module Rake
398  class << self
399
400    # Yield each file or directory component.
401    def each_dir_parent(dir)    # :nodoc:
402      old_length = nil
403      while dir != '.' && dir.length != old_length
404        yield(dir)
405        old_length = dir.length
406        dir = File.dirname(dir)
407      end
408    end
409  end
410end # module Rake
411