# Object-Oriented Pathname Class
# Author:: Tanaka Akira <akr@m17n.org>
# Documentation:: Author and Gavin Sinclair
# For documentation, see class Pathname.
# <tt>pathname.rb</tt> is distributed with Ruby since 1.8.0.
# Pathname represents a pathname which locates a file in a filesystem.
# The pathname depends on OS: Unix, Windows, etc.
# Pathname library works with pathnames of local OS.
# However non-Unix pathnames are supported experimentally.
# It does not represent the file itself.
# A Pathname can be relative or absolute. It's not until you try to
# reference the file that it even matters whether the file exists or not.
# Pathname is immutable. It has no method for destructive update.
# The value of this class is to manipulate file path information in a neater
# way than standard Ruby provides. The examples below demonstrate the
# difference. *All* functionality from File, FileTest, and some from Dir and
# FileUtils is included, in an unsurprising way. It is essentially a facade for
# all of these, and more.
# === Example 1: Using Pathname
# p = Pathname.new("/usr/bin/ruby")
# isdir = p.directory? # false
# dir = p.dirname # Pathname:/usr/bin
# base = p.basename # Pathname:ruby
# dir, base = p.split # [Pathname:/usr/bin, Pathname:ruby]
# p.each_line { |line| _ }
# === Example 2: Using standard Ruby
# size = File.size(p) # 27662
# isdir = File.directory?(p) # false
# dir = File.dirname(p) # "/usr/bin"
# base = File.basename(p) # "ruby"
# dir, base = File.split(p) # ["/usr/bin", "ruby"]
# File.foreach(p) { |line| _ }
# === Example 3: Special features
# p1 = Pathname.new("/usr/lib") # Pathname:/usr/lib
# p2 = p1 + "ruby/1.8" # Pathname:/usr/lib/ruby/1.8
# p3 = p1.parent # Pathname:/usr
# p4 = p2.relative_path_from(p3) # Pathname:lib/ruby/1.8
# pwd = Pathname.pwd # Pathname:/home/gavin
# p5 = Pathname.new "." # Pathname:.
# p5 = p5 + "music/../articles" # Pathname:music/../articles
# p5.cleanpath # Pathname:articles
# p5.realpath # Pathname:/home/gavin/articles
# p5.children # [Pathname:/home/gavin/articles/linux, ...]
# == Breakdown of functionality
# These methods are effectively manipulating a String, because that's all a path
# is. Except for #mountpoint?, #children, and #realpath, they don't access the
# === File status predicate methods
# These methods are a facade for FileTest:
# === File property and manipulation methods
# These methods are a facade for File:
# - #lchown(owner, group)
# - #fnmatch(pattern, *args)
# - #fnmatch?(pattern, *args)
# These methods are a facade for Dir:
# - Pathname.getwd / Pathname.pwd
# These methods are a facade for IO:
# - #each_line(*args, &block)
# These methods are a mixture of Find, FileUtils, and others:
# == Method documentation
# As the above section shows, most of the methods in Pathname are facades. The
# documentation for these methods generally just says, for instance, "See
# FileTest.writable?", as you should be familiar with the original method
# anyway, and its documentation (e.g. through +ri+) will contain more
# information. In some cases, a brief description will follow.
# to_path is implemented so Pathname objects are usable with File.open, etc.
SAME_PATHS = if File::FNM_SYSCASE.nonzero?
proc {|a, b| a.casecmp(b).zero?}
# Create a Pathname object from the given String (or String-like object).
# If +path+ contains a NUL character (<tt>\0</tt>), an ArgumentError is raised.
path = path.__send__(TO_PATH) if path.respond_to? TO_PATH
raise ArgumentError, "pathname contains \\0: #{@path.inspect}"
self.taint if @path.tainted?
def freeze() super; @path.freeze; self end
def taint() super; @path.taint; self end
def untaint() super; @path.untaint; self end
# Compare this pathname with +other+. The comparison is string-based.
# Be aware that two different paths (<tt>foo.txt</tt> and <tt>./foo.txt</tt>)
# can refer to the same file.
return false unless Pathname === other
# Provides for comparing pathnames, case-sensitively.
return nil unless Pathname === other
@path.tr('/', "\0") <=> other.to_s.tr('/', "\0")
# Return the path as a String.
# to_path is implemented so Pathname objects are usable with File.open, etc.
alias_method TO_PATH, :to_s
"#<#{self.class}:#{@path}>"
# Return a pathname which is substituted by String#sub.
def sub(pattern, *rest, &block)
path = @path.sub(pattern, *rest) {|*args|
old = Thread.current[:pathname_sub_matchdata]
Thread.current[:pathname_sub_matchdata] = $~
eval("$~ = Thread.current[:pathname_sub_matchdata]", block.binding)
Thread.current[:pathname_sub_matchdata] = old
path = @path.sub(pattern, *rest)
SEPARATOR_PAT = /[#{Regexp.quote File::ALT_SEPARATOR}#{Regexp.quote File::SEPARATOR}]/
SEPARATOR_PAT = /#{Regexp.quote File::SEPARATOR}/
# chop_basename(path) -> [pre-basename, basename] or nil
base = File.basename(path)
if /\A#{SEPARATOR_PAT}?\z/ =~ base
return path[0, path.rindex(base)], base
# split_names(path) -> prefix, [name, ...]
while r = chop_basename(path)
def prepend_prefix(prefix, relpath)
elsif /#{SEPARATOR_PAT}/ =~ prefix
prefix = File.dirname(prefix)
prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a'
# Returns clean pathname of +self+ with consecutive slashes and useless dots
# removed. The filesystem is not accessed.
# If +consider_symlink+ is +true+, then a more conservative algorithm is used
# to avoid breaking symbolic linkages. This may retain more <tt>..</tt>
# entries than absolutely necessary, but without accessing the filesystem,
# this can't be avoided. See #realpath.
def cleanpath(consider_symlink=false)
# Clean the path simply by resolving and removing excess "." and ".." entries.
# Nothing more, nothing less.
while r = chop_basename(pre)
if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
names.shift while names[0] == '..'
self.class.new(prepend_prefix(pre, File.join(*names)))
private :cleanpath_aggressive
# has_trailing_separator?(path) -> bool
def has_trailing_separator?(path)
if r = chop_basename(path)
pre.length + basename.length < path.length
private :has_trailing_separator?
# add_trailing_separator(path) -> path
def add_trailing_separator(path)
if File.basename(path + 'a') == 'a'
File.join(path, "") # xxx: Is File.join is appropriate to add separator?
private :add_trailing_separator
def del_trailing_separator(path)
if r = chop_basename(path)
elsif /#{SEPARATOR_PAT}+\z/o =~ path
$` + File.dirname(path)[/#{SEPARATOR_PAT}*\z/o]
private :del_trailing_separator
def cleanpath_conservative
while r = chop_basename(pre)
names.unshift base if base != '.'
if /#{SEPARATOR_PAT}/o =~ File.basename(pre)
names.shift while names[0] == '..'
self.class.new(File.dirname(pre))
if names.last != '..' && File.basename(path) == '.'
result = prepend_prefix(pre, File.join(*names))
if /\A(?:\.|\.\.)\z/ !~ names.last && has_trailing_separator?(path)
self.class.new(add_trailing_separator(result))
private :cleanpath_conservative
def realpath_rec(prefix, unresolved, h)
path = prepend_prefix(prefix, File.join(*(resolved + [n])))
raise Errno::ELOOP.new(path)
prefix, *resolved = h[path]
link_prefix, link_names = split_names(File.readlink(path))
prefix, *resolved = h[path] = realpath_rec(prefix, resolved + link_names, h)
prefix, *resolved = h[path] = realpath_rec(link_prefix, link_names, h)
h[path] = [prefix, *resolved]
# Returns a real (absolute) pathname of +self+ in the actual filesystem.
# The real pathname doesn't contain symlinks or useless dots.
# No arguments should be given; the old behaviour is *obsoleted*.
prefix, names = split_names(path)
prefix, names2 = split_names(Dir.pwd)
prefix, *names = realpath_rec(prefix, names, {})
self.class.new(prepend_prefix(prefix, File.join(*names)))
# #parent returns the parent directory.
# This is same as <tt>self + '..'</tt>.
# #mountpoint? returns +true+ if <tt>self</tt> points to a mountpoint.
stat2 = self.parent.lstat
stat1.dev == stat2.dev && stat1.ino == stat2.ino ||
# #root? is a predicate for root directories. I.e. it returns +true+ if the
# pathname consists of consecutive slashes.