1#
2# = pathname.rb
3#
4# Object-Oriented Pathname Class
5#
6# Author:: Tanaka Akira <akr@m17n.org>
7# Documentation:: Author and Gavin Sinclair
8#
9# For documentation, see class Pathname.
10#
11
12require 'pathname.so'
13
14class Pathname
15
16  # :stopdoc:
17  if RUBY_VERSION < "1.9"
18    TO_PATH = :to_str
19  else
20    # to_path is implemented so Pathname objects are usable with File.open, etc.
21    TO_PATH = :to_path
22  end
23
24  SAME_PATHS = if File::FNM_SYSCASE.nonzero?
25    proc {|a, b| a.casecmp(b).zero?}
26  else
27    proc {|a, b| a == b}
28  end
29
30
31  if File::ALT_SEPARATOR
32    SEPARATOR_LIST = "#{Regexp.quote File::ALT_SEPARATOR}#{Regexp.quote File::SEPARATOR}"
33    SEPARATOR_PAT = /[#{SEPARATOR_LIST}]/
34  else
35    SEPARATOR_LIST = "#{Regexp.quote File::SEPARATOR}"
36    SEPARATOR_PAT = /#{Regexp.quote File::SEPARATOR}/
37  end
38
39  # :startdoc:
40
41  # chop_basename(path) -> [pre-basename, basename] or nil
42  def chop_basename(path) # :nodoc:
43    base = File.basename(path)
44    if /\A#{SEPARATOR_PAT}?\z/o =~ base
45      return nil
46    else
47      return path[0, path.rindex(base)], base
48    end
49  end
50  private :chop_basename
51
52  # split_names(path) -> prefix, [name, ...]
53  def split_names(path) # :nodoc:
54    names = []
55    while r = chop_basename(path)
56      path, basename = r
57      names.unshift basename
58    end
59    return path, names
60  end
61  private :split_names
62
63  def prepend_prefix(prefix, relpath) # :nodoc:
64    if relpath.empty?
65      File.dirname(prefix)
66    elsif /#{SEPARATOR_PAT}/o =~ prefix
67      prefix = File.dirname(prefix)
68      prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a'
69      prefix + relpath
70    else
71      prefix + relpath
72    end
73  end
74  private :prepend_prefix
75
76  # Returns clean pathname of +self+ with consecutive slashes and useless dots
77  # removed.  The filesystem is not accessed.
78  #
79  # If +consider_symlink+ is +true+, then a more conservative algorithm is used
80  # to avoid breaking symbolic linkages.  This may retain more +..+
81  # entries than absolutely necessary, but without accessing the filesystem,
82  # this can't be avoided.
83  #
84  # See Pathname#realpath.
85  #
86  def cleanpath(consider_symlink=false)
87    if consider_symlink
88      cleanpath_conservative
89    else
90      cleanpath_aggressive
91    end
92  end
93
94  #
95  # Clean the path simply by resolving and removing excess +.+ and +..+ entries.
96  # Nothing more, nothing less.
97  #
98  def cleanpath_aggressive # :nodoc:
99    path = @path
100    names = []
101    pre = path
102    while r = chop_basename(pre)
103      pre, base = r
104      case base
105      when '.'
106      when '..'
107        names.unshift base
108      else
109        if names[0] == '..'
110          names.shift
111        else
112          names.unshift base
113        end
114      end
115    end
116    if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
117      names.shift while names[0] == '..'
118    end
119    self.class.new(prepend_prefix(pre, File.join(*names)))
120  end
121  private :cleanpath_aggressive
122
123  # has_trailing_separator?(path) -> bool
124  def has_trailing_separator?(path) # :nodoc:
125    if r = chop_basename(path)
126      pre, basename = r
127      pre.length + basename.length < path.length
128    else
129      false
130    end
131  end
132  private :has_trailing_separator?
133
134  # add_trailing_separator(path) -> path
135  def add_trailing_separator(path) # :nodoc:
136    if File.basename(path + 'a') == 'a'
137      path
138    else
139      File.join(path, "") # xxx: Is File.join is appropriate to add separator?
140    end
141  end
142  private :add_trailing_separator
143
144  def del_trailing_separator(path) # :nodoc:
145    if r = chop_basename(path)
146      pre, basename = r
147      pre + basename
148    elsif /#{SEPARATOR_PAT}+\z/o =~ path
149      $` + File.dirname(path)[/#{SEPARATOR_PAT}*\z/o]
150    else
151      path
152    end
153  end
154  private :del_trailing_separator
155
156  def cleanpath_conservative # :nodoc:
157    path = @path
158    names = []
159    pre = path
160    while r = chop_basename(pre)
161      pre, base = r
162      names.unshift base if base != '.'
163    end
164    if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
165      names.shift while names[0] == '..'
166    end
167    if names.empty?
168      self.class.new(File.dirname(pre))
169    else
170      if names.last != '..' && File.basename(path) == '.'
171        names << '.'
172      end
173      result = prepend_prefix(pre, File.join(*names))
174      if /\A(?:\.|\.\.)\z/ !~ names.last && has_trailing_separator?(path)
175        self.class.new(add_trailing_separator(result))
176      else
177        self.class.new(result)
178      end
179    end
180  end
181  private :cleanpath_conservative
182
183  # Returns the parent directory.
184  #
185  # This is same as <code>self + '..'</code>.
186  def parent
187    self + '..'
188  end
189
190  # Returns +true+ if +self+ points to a mountpoint.
191  def mountpoint?
192    begin
193      stat1 = self.lstat
194      stat2 = self.parent.lstat
195      stat1.dev == stat2.dev && stat1.ino == stat2.ino ||
196        stat1.dev != stat2.dev
197    rescue Errno::ENOENT
198      false
199    end
200  end
201
202  #
203  # Predicate method for root directories.  Returns +true+ if the
204  # pathname consists of consecutive slashes.
205  #
206  # It doesn't access the filesystem.  So it may return +false+ for some
207  # pathnames which points to roots such as <tt>/usr/..</tt>.
208  #
209  def root?
210    !!(chop_basename(@path) == nil && /#{SEPARATOR_PAT}/o =~ @path)
211  end
212
213  # Predicate method for testing whether a path is absolute.
214  #
215  # It returns +true+ if the pathname begins with a slash.
216  #
217  #   p = Pathname.new('/im/sure')
218  #   p.absolute?
219  #       #=> true
220  #
221  #   p = Pathname.new('not/so/sure')
222  #   p.absolute?
223  #       #=> false
224  def absolute?
225    !relative?
226  end
227
228  # The opposite of Pathname#absolute?
229  #
230  # It returns +false+ if the pathname begins with a slash.
231  #
232  #   p = Pathname.new('/im/sure')
233  #   p.relative?
234  #       #=> false
235  #
236  #   p = Pathname.new('not/so/sure')
237  #   p.relative?
238  #       #=> true
239  def relative?
240    path = @path
241    while r = chop_basename(path)
242      path, = r
243    end
244    path == ''
245  end
246
247  #
248  # Iterates over each component of the path.
249  #
250  #   Pathname.new("/usr/bin/ruby").each_filename {|filename| ... }
251  #     # yields "usr", "bin", and "ruby".
252  #
253  # Returns an Enumerator if no block was given.
254  #
255  #   enum = Pathname.new("/usr/bin/ruby").each_filename
256  #     # ... do stuff ...
257  #   enum.each { |e| ... }
258  #     # yields "usr", "bin", and "ruby".
259  #
260  def each_filename # :yield: filename
261    return to_enum(__method__) unless block_given?
262    _, names = split_names(@path)
263    names.each {|filename| yield filename }
264    nil
265  end
266
267  # Iterates over and yields a new Pathname object
268  # for each element in the given path in descending order.
269  #
270  #  Pathname.new('/path/to/some/file.rb').descend {|v| p v}
271  #     #<Pathname:/>
272  #     #<Pathname:/path>
273  #     #<Pathname:/path/to>
274  #     #<Pathname:/path/to/some>
275  #     #<Pathname:/path/to/some/file.rb>
276  #
277  #  Pathname.new('path/to/some/file.rb').descend {|v| p v}
278  #     #<Pathname:path>
279  #     #<Pathname:path/to>
280  #     #<Pathname:path/to/some>
281  #     #<Pathname:path/to/some/file.rb>
282  #
283  # It doesn't access the filesystem.
284  #
285  def descend
286    vs = []
287    ascend {|v| vs << v }
288    vs.reverse_each {|v| yield v }
289    nil
290  end
291
292  # Iterates over and yields a new Pathname object
293  # for each element in the given path in ascending order.
294  #
295  #  Pathname.new('/path/to/some/file.rb').ascend {|v| p v}
296  #     #<Pathname:/path/to/some/file.rb>
297  #     #<Pathname:/path/to/some>
298  #     #<Pathname:/path/to>
299  #     #<Pathname:/path>
300  #     #<Pathname:/>
301  #
302  #  Pathname.new('path/to/some/file.rb').ascend {|v| p v}
303  #     #<Pathname:path/to/some/file.rb>
304  #     #<Pathname:path/to/some>
305  #     #<Pathname:path/to>
306  #     #<Pathname:path>
307  #
308  # It doesn't access the filesystem.
309  #
310  def ascend
311    path = @path
312    yield self
313    while r = chop_basename(path)
314      path, = r
315      break if path.empty?
316      yield self.class.new(del_trailing_separator(path))
317    end
318  end
319
320  #
321  # Appends a pathname fragment to +self+ to produce a new Pathname object.
322  #
323  #   p1 = Pathname.new("/usr")      # Pathname:/usr
324  #   p2 = p1 + "bin/ruby"           # Pathname:/usr/bin/ruby
325  #   p3 = p1 + "/etc/passwd"        # Pathname:/etc/passwd
326  #
327  # This method doesn't access the file system; it is pure string manipulation.
328  #
329  def +(other)
330    other = Pathname.new(other) unless Pathname === other
331    Pathname.new(plus(@path, other.to_s))
332  end
333
334  def plus(path1, path2) # -> path # :nodoc:
335    prefix2 = path2
336    index_list2 = []
337    basename_list2 = []
338    while r2 = chop_basename(prefix2)
339      prefix2, basename2 = r2
340      index_list2.unshift prefix2.length
341      basename_list2.unshift basename2
342    end
343    return path2 if prefix2 != ''
344    prefix1 = path1
345    while true
346      while !basename_list2.empty? && basename_list2.first == '.'
347        index_list2.shift
348        basename_list2.shift
349      end
350      break unless r1 = chop_basename(prefix1)
351      prefix1, basename1 = r1
352      next if basename1 == '.'
353      if basename1 == '..' || basename_list2.empty? || basename_list2.first != '..'
354        prefix1 = prefix1 + basename1
355        break
356      end
357      index_list2.shift
358      basename_list2.shift
359    end
360    r1 = chop_basename(prefix1)
361    if !r1 && /#{SEPARATOR_PAT}/o =~ File.basename(prefix1)
362      while !basename_list2.empty? && basename_list2.first == '..'
363        index_list2.shift
364        basename_list2.shift
365      end
366    end
367    if !basename_list2.empty?
368      suffix2 = path2[index_list2.first..-1]
369      r1 ? File.join(prefix1, suffix2) : prefix1 + suffix2
370    else
371      r1 ? prefix1 : File.dirname(prefix1)
372    end
373  end
374  private :plus
375
376  #
377  # Joins the given pathnames onto +self+ to create a new Pathname object.
378  #
379  #   path0 = Pathname.new("/usr")                # Pathname:/usr
380  #   path0 = path0.join("bin/ruby")              # Pathname:/usr/bin/ruby
381  #       # is the same as
382  #   path1 = Pathname.new("/usr") + "bin/ruby"   # Pathname:/usr/bin/ruby
383  #   path0 == path1
384  #       #=> true
385  #
386  def join(*args)
387    args.unshift self
388    result = args.pop
389    result = Pathname.new(result) unless Pathname === result
390    return result if result.absolute?
391    args.reverse_each {|arg|
392      arg = Pathname.new(arg) unless Pathname === arg
393      result = arg + result
394      return result if result.absolute?
395    }
396    result
397  end
398
399  #
400  # Returns the children of the directory (files and subdirectories, not
401  # recursive) as an array of Pathname objects.
402  #
403  # By default, the returned pathnames will have enough information to access
404  # the files. If you set +with_directory+ to +false+, then the returned
405  # pathnames will contain the filename only.
406  #
407  # For example:
408  #   pn = Pathname("/usr/lib/ruby/1.8")
409  #   pn.children
410  #       # -> [ Pathname:/usr/lib/ruby/1.8/English.rb,
411  #              Pathname:/usr/lib/ruby/1.8/Env.rb,
412  #              Pathname:/usr/lib/ruby/1.8/abbrev.rb, ... ]
413  #   pn.children(false)
414  #       # -> [ Pathname:English.rb, Pathname:Env.rb, Pathname:abbrev.rb, ... ]
415  #
416  # Note that the results never contain the entries +.+ and +..+ in
417  # the directory because they are not children.
418  #
419  def children(with_directory=true)
420    with_directory = false if @path == '.'
421    result = []
422    Dir.foreach(@path) {|e|
423      next if e == '.' || e == '..'
424      if with_directory
425        result << self.class.new(File.join(@path, e))
426      else
427        result << self.class.new(e)
428      end
429    }
430    result
431  end
432
433  # Iterates over the children of the directory
434  # (files and subdirectories, not recursive).
435  #
436  # It yields Pathname object for each child.
437  #
438  # By default, the yielded pathnames will have enough information to access
439  # the files.
440  #
441  # If you set +with_directory+ to +false+, then the returned pathnames will
442  # contain the filename only.
443  #
444  #   Pathname("/usr/local").each_child {|f| p f }
445  #   #=> #<Pathname:/usr/local/share>
446  #   #   #<Pathname:/usr/local/bin>
447  #   #   #<Pathname:/usr/local/games>
448  #   #   #<Pathname:/usr/local/lib>
449  #   #   #<Pathname:/usr/local/include>
450  #   #   #<Pathname:/usr/local/sbin>
451  #   #   #<Pathname:/usr/local/src>
452  #   #   #<Pathname:/usr/local/man>
453  #
454  #   Pathname("/usr/local").each_child(false) {|f| p f }
455  #   #=> #<Pathname:share>
456  #   #   #<Pathname:bin>
457  #   #   #<Pathname:games>
458  #   #   #<Pathname:lib>
459  #   #   #<Pathname:include>
460  #   #   #<Pathname:sbin>
461  #   #   #<Pathname:src>
462  #   #   #<Pathname:man>
463  #
464  # Note that the results never contain the entries +.+ and +..+ in
465  # the directory because they are not children.
466  #
467  # See Pathname#children
468  #
469  def each_child(with_directory=true, &b)
470    children(with_directory).each(&b)
471  end
472
473  #
474  # Returns a relative path from the given +base_directory+ to the receiver.
475  #
476  # If +self+ is absolute, then +base_directory+ must be absolute too.
477  #
478  # If +self+ is relative, then +base_directory+ must be relative too.
479  #
480  # This method doesn't access the filesystem.  It assumes no symlinks.
481  #
482  # ArgumentError is raised when it cannot find a relative path.
483  #
484  def relative_path_from(base_directory)
485    dest_directory = self.cleanpath.to_s
486    base_directory = base_directory.cleanpath.to_s
487    dest_prefix = dest_directory
488    dest_names = []
489    while r = chop_basename(dest_prefix)
490      dest_prefix, basename = r
491      dest_names.unshift basename if basename != '.'
492    end
493    base_prefix = base_directory
494    base_names = []
495    while r = chop_basename(base_prefix)
496      base_prefix, basename = r
497      base_names.unshift basename if basename != '.'
498    end
499    unless SAME_PATHS[dest_prefix, base_prefix]
500      raise ArgumentError, "different prefix: #{dest_prefix.inspect} and #{base_directory.inspect}"
501    end
502    while !dest_names.empty? &&
503          !base_names.empty? &&
504          SAME_PATHS[dest_names.first, base_names.first]
505      dest_names.shift
506      base_names.shift
507    end
508    if base_names.include? '..'
509      raise ArgumentError, "base_directory has ..: #{base_directory.inspect}"
510    end
511    base_names.fill('..')
512    relpath_names = base_names + dest_names
513    if relpath_names.empty?
514      Pathname.new('.')
515    else
516      Pathname.new(File.join(*relpath_names))
517    end
518  end
519end
520
521
522class Pathname    # * Find *
523  #
524  # Iterates over the directory tree in a depth first manner, yielding a
525  # Pathname for each file under "this" directory.
526  #
527  # Returns an Enumerator if no block is given.
528  #
529  # Since it is implemented by the standard library module Find, Find.prune can
530  # be used to control the traversal.
531  #
532  # If +self+ is +.+, yielded pathnames begin with a filename in the
533  # current directory, not +./+.
534  #
535  # See Find.find
536  #
537  def find # :yield: pathname
538    return to_enum(__method__) unless block_given?
539    require 'find'
540    if @path == '.'
541      Find.find(@path) {|f| yield self.class.new(f.sub(%r{\A\./}, '')) }
542    else
543      Find.find(@path) {|f| yield self.class.new(f) }
544    end
545  end
546end
547
548
549class Pathname    # * FileUtils *
550  # Creates a full path, including any intermediate directories that don't yet
551  # exist.
552  #
553  # See FileUtils.mkpath and FileUtils.mkdir_p
554  def mkpath
555    require 'fileutils'
556    FileUtils.mkpath(@path)
557    nil
558  end
559
560  # Recursively deletes a directory, including all directories beneath it.
561  #
562  # See FileUtils.rm_r
563  def rmtree
564    # The name "rmtree" is borrowed from File::Path of Perl.
565    # File::Path provides "mkpath" and "rmtree".
566    require 'fileutils'
567    FileUtils.rm_r(@path)
568    nil
569  end
570end
571
572