# Object-Oriented Pathname Class # # Author:: Tanaka Akira # Pathname represents a pathname which locates a file in a filesystem. # It supports only Unix style pathnames. # # Pathname is immutable. It has no method for destructive update. # # pathname.rb is distributed with Ruby since 1.8.0. class Pathname def initialize(path) @path = path.to_str.dup @path.freeze if /\0/ =~ @path raise ArgumentError, "pathname contains \\0: #{@path.inspect}" end end def ==(other) return false unless Pathname === other other.to_s == @path end alias === == alias eql? == def <=>(other) return nil unless Pathname === other @path.tr('/', "\0") <=> other.to_s.tr('/', "\0") end def hash @path.hash end def to_s @path.dup end # to_str is implemented for Pathname object usable with File.open, etc. alias to_str to_s def inspect "#<#{self.class}:#{@path}>" end # cleanpath returns clean pathname of self which is without consecutive # slashes and useless dots. # # If true is given as the optional argument consider_symlink, # symbolic links are considered. It makes more dots are retained. # # cleanpath doesn't access actual filesystem. def cleanpath(consider_symlink=false) if consider_symlink cleanpath_conservative else cleanpath_aggressive end end def cleanpath_aggressive # :nodoc: # cleanpath_aggressive assumes: # * no symlink # * all pathname prefix contained in the pathname is existing directory return Pathname.new('') if @path == '' absolute = absolute? names = [] @path.scan(%r{[^/]+}) {|name| next if name == '.' if name == '..' if names.empty? next if absolute else if names.last != '..' names.pop next end end end names << name } return Pathname.new(absolute ? '/' : '.') if names.empty? path = absolute ? '/' : '' path << names.join('/') Pathname.new(path) end def cleanpath_conservative # :nodoc: return Pathname.new('') if @path == '' names = @path.scan(%r{[^/]+}) last_dot = names.last == '.' names.delete('.') names.shift while names.first == '..' if absolute? return Pathname.new(absolute? ? '/' : '.') if names.empty? path = absolute? ? '/' : '' path << names.join('/') if names.last != '..' if last_dot path << '/.' elsif %r{/\z} =~ @path path << '/' end end Pathname.new(path) end # realpath returns a real pathname of self in actual filesystem. # The real pathname doesn't contain a symlink and useless dots. # # It returns absolute pathname. def realpath(*args) unless args.empty? warn "The argument for Pathname#realpath is obsoleted." end force_absolute = args.fetch(0, true) if %r{\A/} =~ @path top = '/' unresolved = @path.scan(%r{[^/]+}) elsif force_absolute # Although POSIX getcwd returns a pathname which contains no symlink, # 4.4BSD-Lite2 derived getcwd may return the environment variable $PWD # which may contain a symlink. # So the return value of Dir.pwd should be examined. top = '/' unresolved = Dir.pwd.scan(%r{[^/]+}) + @path.scan(%r{[^/]+}) else top = '' unresolved = @path.scan(%r{[^/]+}) end resolved = [] until unresolved.empty? case unresolved.last when '.' unresolved.pop when '..' resolved.unshift unresolved.pop else loop_check = {} while (stat = File.lstat(path = top + unresolved.join('/'))).symlink? symlink_id = "#{stat.dev}:#{stat.ino}" raise Errno::ELOOP.new(path) if loop_check[symlink_id] loop_check[symlink_id] = true if %r{\A/} =~ (link = File.readlink(path)) top = '/' unresolved = link.scan(%r{[^/]+}) else unresolved[-1,1] = link.scan(%r{[^/]+}) end end next if (filename = unresolved.pop) == '.' if filename != '..' && resolved.first == '..' resolved.shift else resolved.unshift filename end end end if top == '/' resolved.shift while resolved[0] == '..' end if resolved.empty? Pathname.new(top.empty? ? '.' : '/') else Pathname.new(top + resolved.join('/')) end end # parent method returns parent directory. # # If self is `.', `..' is returned. # Otherwise, `..' is joined to self. def parent if @path == '.' Pathname.new('..') else self.join('..') end end # mountpoint? method returns true if self points a mountpoint. def mountpoint? begin stat1 = self.lstat stat2 = self.parent.lstat stat1.dev == stat2.dev && stat1.ino == stat2.ino || stat1.dev != stat2.dev rescue Errno::ENOENT false end end # root? method is a predicate for root directory. # I.e. it returns true if the pathname consists of consecutive slashes. # # It doesn't access actual filesystem. # So it may return false for some pathnames # which points root such as "/usr/..". def root? %r{\A/+\z} =~ @path ? true : false end # absolute? method is a predicate for absolute pathname. # It returns true if self is beginning with a slash. def absolute? %r{\A/} =~ @path ? true : false end # relative? method is a predicate for relative pathname. # It returns true unless self is beginning with a slash. def relative? !absolute? end # each_filename iterates over self for each filename components. def each_filename @path.scan(%r{[^/]+}) { yield $& } end # Pathname#+ return new pathname which is concatenated with self and # an argument. # If self is the current working directory `.' or # the argument is absolute pathname, # the argument is just returned. # If the argument is `.', self is returned. def +(other) other = Pathname.new(other) unless Pathname === other if @path == '.' || other.absolute? other elsif other.to_s == '.' self elsif %r{/\z} =~ @path Pathname.new(@path + other.to_s) else Pathname.new(@path + '/' + other.to_s) end end # Pathname#join joins pathnames. # # path0.join(path1, ... pathN) is same as path0 + path1 + ... + pathN. def join(*args) args.map! {|arg| Pathname === arg ? arg : Pathname.new(arg) } args.inject(self) {|pathname, arg| pathname + arg } end # Pathname#children returns the children of the directory as an array of # pathnames. # # By default, the returned pathname can be used to access the corresponding # file in the directory. # This is because the pathname contains self as a prefix unless self is `.'. # # If false is given for the optional argument `with_directory', # just filenames of children is returned. # In this case, the returned pathname cannot be used directly to access the # corresponding file when self doesn't point working directory. # # Note that the result never contain the entry `.' and `..' in the directory # because they are not child. # # This method is exist since 1.8.1. def children(with_directory=true) with_directory = false if @path == '.' result = [] Dir.foreach(@path) {|e| next if e == '.' || e == '..' if with_directory result << Pathname.new(File.join(@path, e)) else result << Pathname.new(e) end } result end # Pathname#relative_path_from returns a relative path from the argument to # self. # If self is absolute, the argument must be absolute too. # If self is relative, the argument must be relative too. # # relative_path_from doesn't access actual filesystem. # It assumes no symlinks. # # ArgumentError is raised when it cannot find a relative path. # # This method is exist since 1.8.1. def relative_path_from(base_directory) if self.absolute? != base_directory.absolute? raise ArgumentError, "relative path between absolute and relative path: #{self.inspect}, #{base_directory.inspect}" end dest = [] self.cleanpath.each_filename {|f| next if f == '.' dest << f } base = [] base_directory.cleanpath.each_filename {|f| next if f == '.' base << f } while !base.empty? && !dest.empty? && base[0] == dest[0] base.shift dest.shift end if base.include? '..' raise ArgumentError, "base_directory has ..: #{base_directory.inspect}" end base.fill '..' relpath = base + dest if relpath.empty? Pathname.new(".") else Pathname.new(relpath.join('/')) end end end # IO class Pathname # Pathname#each_line iterates over lines of the file. # It's yields a String object for each line. # # This method is exist since 1.8.1. def each_line(*args, &block) IO.foreach(@path, *args, &block) end # Pathname#foreachline is obsoleted at 1.8.1. # def foreachline(*args, &block) # compatibility to 1.8.0. obsoleted. warn "Pathname#foreachline is obsoleted. Use Pathname#each_line." each_line(*args, &block) end def read(*args) IO.read(@path, *args) end def readlines(*args) IO.readlines(@path, *args) end def sysopen(*args) IO.sysopen(@path, *args) end end # File class Pathname def atime() File.atime(@path) end def ctime() File.ctime(@path) end def mtime() File.mtime(@path) end def chmod(mode) File.chmod(mode, @path) end def lchmod(mode) File.chmod(mode, @path) end def chown(owner, group) File.chown(owner, group, @path) end def lchown(owner, group) File.lchown(owner, group, @path) end def fnmatch(pattern, *args) File.fnmatch(pattern, @path, *args) end def fnmatch?(pattern, *args) File.fnmatch?(pattern, @path, *args) end def ftype() File.ftype(@path) end def make_link(old) File.link(old, @path) end def open(*args, &block) File.open(@path, *args, &block) end def readlink() Pathname.new(File.readlink(@path)) end def rename(to) File.rename(@path, to) end def stat() File.stat(@path) end def lstat() File.lstat(@path) end def make_symlink(old) File.symlink(old, @path) end def truncate(length) File.truncate(@path, length) end def utime(atime, mtime) File.utime(atime, mtime, @path) end def basename(*args) Pathname.new(File.basename(@path, *args)) end def dirname() Pathname.new(File.dirname(@path)) end def extname() File.extname(@path) end def expand_path(*args) Pathname.new(File.expand_path(@path, *args)) end def split() File.split(@path).map {|f| Pathname.new(f) } end # Pathname#link is confusing and obsoleted because the receiver/argument # order is inverted to corresponding system call. def link(old) warn 'Pathname#link is obsoleted. Use Pathname#make_link.' File.link(old, @path) end # Pathname#symlink is confusing and obsoleted because the receiver/argument # order is inverted to corresponding system call. def symlink(old) warn 'Pathname#symlink is obsoleted. Use Pathname#make_symlink.' File.symlink(old, @path) end end # FileTest class Pathname def blockdev?() FileTest.blockdev?(@path) end def chardev?() FileTest.chardev?(@path) end def executable?() FileTest.executable?(@path) end def executable_real?() FileTest.executable_real?(@path) end def exist?() FileTest.exist?(@path) end def grpowned?() FileTest.grpowned?(@path) end def directory?() FileTest.directory?(@path) end def file?() FileTest.file?(@path) end def pipe?() FileTest.pipe?(@path) end def socket?() FileTest.socket?(@path) end def owned?() FileTest.owned?(@path) end def readable?() FileTest.readable?(@path) end def readable_real?() FileTest.readable_real?(@path) end def setuid?() FileTest.setuid?(@path) end def setgid?() FileTest.setgid?(@path) end def size() FileTest.size(@path) end def size?() FileTest.size?(@path) end def sticky?() FileTest.sticky?(@path) end def symlink?() FileTest.symlink?(@path) end def writable?() FileTest.writable?(@path) end def writable_real?() FileTest.writable_real?(@path) end def zero?() FileTest.zero?(@path) end end # Dir class Pathname def Pathname.glob(*args) if block_given? Dir.glob(*args) {|f| yield Pathname.new(f) } else Dir.glob(*args).map {|f| Pathname.new(f) } end end def Pathname.getwd() Pathname.new(Dir.getwd) end class << self; alias pwd getwd end # Pathname#chdir is obsoleted at 1.8.1. # def chdir(&block) # compatibility to 1.8.0. warn "Pathname#chdir is obsoleted. Use Dir.chdir." Dir.chdir(@path, &block) end # Pathname#chroot is obsoleted at 1.8.1. # def chroot # compatibility to 1.8.0. warn "Pathname#chroot is obsoleted. Use Dir.chroot." Dir.chroot(@path) end def rmdir() Dir.rmdir(@path) end def entries() Dir.entries(@path).map {|f| Pathname.new(f) } end # Pathname#each_entry iterates over entries of the directory. # It's yields Pathname objects for each entry. # # This method is exist since 1.8.1. def each_entry(&block) Dir.foreach(@path) {|f| yield Pathname.new(f) } end # Pathname#dir_foreach is obsoleted at 1.8.1. # def dir_foreach(*args, &block) # compatibility to 1.8.0. obsoleted. warn "Pathname#dir_foreach is obsoleted. Use Pathname#each_entry." each_entry(*args, &block) end def mkdir(*args) Dir.mkdir(@path, *args) end def opendir(&block) Dir.open(@path, &block) end end # Find class Pathname # Pathname#find is a iterator to traverse directory tree in depth first # manner. It yields a pathname for each file under the directory which # is pointed by self. # # Since it is implemented by find.rb, Find.prune can be used to control the # traverse. # # If self is `.', yielded pathnames begin with a filename in the current # directory, not `./'. def find(&block) require 'find' if @path == '.' Find.find(@path) {|f| yield Pathname.new(f.sub(%r{\A\./}, '')) } else Find.find(@path) {|f| yield Pathname.new(f) } end end end # FileUtils class Pathname def mkpath require 'fileutils' FileUtils.mkpath(@path) nil end def rmtree # The name "rmtree" is borrowed from File::Path of Perl. # File::Path provides "mkpath" and "rmtree". require 'fileutils' FileUtils.rm_r(@path) nil end end # mixed class Pathname def unlink() if FileTest.directory? @path Dir.unlink @path else File.unlink @path end end alias delete unlink # This method is obsoleted at 1.8.1. # def foreach(*args, &block) # compatibility to 1.8.0. obsoleted. warn "Pathname#foreach is obsoleted. Use each_line or each_entry." if FileTest.directory? @path # For polymorphism between Dir.foreach and IO.foreach, # Pathname#foreach doesn't yield Pathname object. Dir.foreach(@path, *args, &block) else IO.foreach(@path, *args, &block) end end end if $0 == __FILE__ require 'test/unit' class PathnameTest < Test::Unit::TestCase # :nodoc: class AnotherStringLike # :nodoc: def initialize(s) @s = s end def to_str() @s end def ==(other) @s == other end end def test_equality obj = Pathname.new("a") str = "a" sym = :a ano = AnotherStringLike.new("a") assert_equal(false, obj == str) assert_equal(false, str == obj) assert_equal(false, obj == ano) assert_equal(false, ano == obj) assert_equal(false, obj == sym) assert_equal(false, sym == obj) obj2 = Pathname.new("a") assert_equal(true, obj == obj2) assert_equal(true, obj === obj2) assert_equal(true, obj.eql?(obj2)) end def test_hashkey h = {} h[Pathname.new("a")] = 1 h[Pathname.new("a")] = 2 assert_equal(1, h.size) end def assert_pathname_cmp(e, s1, s2) p1 = Pathname.new(s1) p2 = Pathname.new(s2) r = p1 <=> p2 assert(e == r, "#{p1.inspect} <=> #{p2.inspect}: <#{e}> expected but was <#{r}>") end def test_comparison assert_pathname_cmp( 0, "a", "a") assert_pathname_cmp( 1, "b", "a") assert_pathname_cmp(-1, "a", "b") ss = %w( a a/ a/b a. a0 ) s1 = ss.shift ss.each {|s2| assert_pathname_cmp(-1, s1, s2) s1 = s2 } end def test_comparison_string assert_equal(nil, Pathname.new("a") <=> "a") assert_equal(nil, "a" <=> Pathname.new("a")) end def test_syntactical assert_equal(true, Pathname.new("/").root?) assert_equal(true, Pathname.new("//").root?) assert_equal(true, Pathname.new("///").root?) assert_equal(false, Pathname.new("").root?) assert_equal(false, Pathname.new("a").root?) end def test_cleanpath assert_equal('/', Pathname.new('/').cleanpath(true).to_s) assert_equal('/', Pathname.new('//').cleanpath(true).to_s) assert_equal('', Pathname.new('').cleanpath(true).to_s) assert_equal('.', Pathname.new('.').cleanpath(true).to_s) assert_equal('..', Pathname.new('..').cleanpath(true).to_s) assert_equal('a', Pathname.new('a').cleanpath(true).to_s) assert_equal('/', Pathname.new('/.').cleanpath(true).to_s) assert_equal('/', Pathname.new('/..').cleanpath(true).to_s) assert_equal('/a', Pathname.new('/a').cleanpath(true).to_s) assert_equal('.', Pathname.new('./').cleanpath(true).to_s) assert_equal('..', Pathname.new('../').cleanpath(true).to_s) assert_equal('a/', Pathname.new('a/').cleanpath(true).to_s) assert_equal('a/b', Pathname.new('a//b').cleanpath(true).to_s) assert_equal('a/.', Pathname.new('a/.').cleanpath(true).to_s) assert_equal('a/.', Pathname.new('a/./').cleanpath(true).to_s) assert_equal('a/..', Pathname.new('a/../').cleanpath(true).to_s) assert_equal('/a/.', Pathname.new('/a/.').cleanpath(true).to_s) assert_equal('..', Pathname.new('./..').cleanpath(true).to_s) assert_equal('..', Pathname.new('../.').cleanpath(true).to_s) assert_equal('..', Pathname.new('./../').cleanpath(true).to_s) assert_equal('..', Pathname.new('.././').cleanpath(true).to_s) assert_equal('/', Pathname.new('/./..').cleanpath(true).to_s) assert_equal('/', Pathname.new('/../.').cleanpath(true).to_s) assert_equal('/', Pathname.new('/./../').cleanpath(true).to_s) assert_equal('/', Pathname.new('/.././').cleanpath(true).to_s) assert_equal('a/b/c', Pathname.new('a/b/c').cleanpath(true).to_s) assert_equal('b/c', Pathname.new('./b/c').cleanpath(true).to_s) assert_equal('a/c', Pathname.new('a/./c').cleanpath(true).to_s) assert_equal('a/b/.', Pathname.new('a/b/.').cleanpath(true).to_s) assert_equal('a/..', Pathname.new('a/../.').cleanpath(true).to_s) assert_equal('/a', Pathname.new('/../.././../a').cleanpath(true).to_s) assert_equal('a/b/../../../../c/../d', Pathname.new('a/b/../../../../c/../d').cleanpath(true).to_s) end def test_cleanpath_no_symlink assert_equal('/', Pathname.new('/').cleanpath.to_s) assert_equal('/', Pathname.new('//').cleanpath.to_s) assert_equal('', Pathname.new('').cleanpath.to_s) assert_equal('.', Pathname.new('.').cleanpath.to_s) assert_equal('..', Pathname.new('..').cleanpath.to_s) assert_equal('a', Pathname.new('a').cleanpath.to_s) assert_equal('/', Pathname.new('/.').cleanpath.to_s) assert_equal('/', Pathname.new('/..').cleanpath.to_s) assert_equal('/a', Pathname.new('/a').cleanpath.to_s) assert_equal('.', Pathname.new('./').cleanpath.to_s) assert_equal('..', Pathname.new('../').cleanpath.to_s) assert_equal('a', Pathname.new('a/').cleanpath.to_s) assert_equal('a/b', Pathname.new('a//b').cleanpath.to_s) assert_equal('a', Pathname.new('a/.').cleanpath.to_s) assert_equal('a', Pathname.new('a/./').cleanpath.to_s) assert_equal('.', Pathname.new('a/../').cleanpath.to_s) assert_equal('/a', Pathname.new('/a/.').cleanpath.to_s) assert_equal('..', Pathname.new('./..').cleanpath.to_s) assert_equal('..', Pathname.new('../.').cleanpath.to_s) assert_equal('..', Pathname.new('./../').cleanpath.to_s) assert_equal('..', Pathname.new('.././').cleanpath.to_s) assert_equal('/', Pathname.new('/./..').cleanpath.to_s) assert_equal('/', Pathname.new('/../.').cleanpath.to_s) assert_equal('/', Pathname.new('/./../').cleanpath.to_s) assert_equal('/', Pathname.new('/.././').cleanpath.to_s) assert_equal('a/b/c', Pathname.new('a/b/c').cleanpath.to_s) assert_equal('b/c', Pathname.new('./b/c').cleanpath.to_s) assert_equal('a/c', Pathname.new('a/./c').cleanpath.to_s) assert_equal('a/b', Pathname.new('a/b/.').cleanpath.to_s) assert_equal('.', Pathname.new('a/../.').cleanpath.to_s) assert_equal('/a', Pathname.new('/../.././../a').cleanpath.to_s) assert_equal('../../d', Pathname.new('a/b/../../../../c/../d').cleanpath.to_s) end def test_destructive_update path = Pathname.new("a") path.to_s.replace "b" assert_equal(Pathname.new("a"), path) end def test_null_character assert_raises(ArgumentError) { Pathname.new("\0") } end def assert_relpath(result, dest, base) assert_equal(Pathname.new(result), Pathname.new(dest).relative_path_from(Pathname.new(base))) end def assert_relpath_err(dest, base) assert_raises(ArgumentError) { Pathname.new(dest).relative_path_from(Pathname.new(base)) } end def test_relative_path_from assert_relpath("../a", "a", "b") assert_relpath("../a", "a", "b/") assert_relpath("../a", "a/", "b") assert_relpath("../a", "a/", "b/") assert_relpath("../a", "/a", "/b") assert_relpath("../a", "/a", "/b/") assert_relpath("../a", "/a/", "/b") assert_relpath("../a", "/a/", "/b/") assert_relpath("../b", "a/b", "a/c") assert_relpath("../a", "../a", "../b") assert_relpath("a", "a", ".") assert_relpath("..", ".", "a") assert_relpath(".", ".", ".") assert_relpath(".", "..", "..") assert_relpath("..", "..", ".") assert_relpath("c/d", "/a/b/c/d", "/a/b") assert_relpath("../..", "/a/b", "/a/b/c/d") assert_relpath("../../../../e", "/e", "/a/b/c/d") assert_relpath("../b/c", "a/b/c", "a/d") assert_relpath("../a", "/../a", "/b") assert_relpath("../../a", "../a", "b") assert_relpath(".", "/a/../../b", "/b") assert_relpath("..", "a/..", "a") assert_relpath(".", "a/../b", "b") assert_relpath("a", "a", "b/..") assert_relpath("b/c", "b/c", "b/..") assert_relpath_err("/", ".") assert_relpath_err(".", "/") assert_relpath_err("a", "..") assert_relpath_err(".", "..") end def test_plus assert_equal(Pathname.new('a/b'), Pathname.new('a') + Pathname.new('b')) assert_equal(Pathname.new('a'), Pathname.new('a') + Pathname.new('.')) assert_equal(Pathname.new('b'), Pathname.new('.') + Pathname.new('b')) assert_equal(Pathname.new('.'), Pathname.new('.') + Pathname.new('.')) assert_equal(Pathname.new('/b'), Pathname.new('a') + Pathname.new('/b')) end end end