diff options
Diffstat (limited to 'pathname_builtin.rb')
| -rw-r--r-- | pathname_builtin.rb | 1895 |
1 files changed, 1895 insertions, 0 deletions
diff --git a/pathname_builtin.rb b/pathname_builtin.rb new file mode 100644 index 0000000000..11ade220f0 --- /dev/null +++ b/pathname_builtin.rb @@ -0,0 +1,1895 @@ +# frozen_string_literal: true +# +# A \Pathname object contains a string directory path or filepath; +# it does not represent a corresponding actual file or directory +# -- which in fact may or may not exist. +# +# A \Pathname object is immutable (except for method #freeze). +# +# A pathname may be relative or absolute: +# +# Pathname.new('lib') # => #<Pathname:lib> +# Pathname.new('/usr/local/bin') # => #<Pathname:/usr/local/bin> +# +# == About the Examples +# +# Many examples here use these variables: +# +# :include: doc/examples/files.rdoc +# +# == Convenience Methods +# +# The class provides *all* functionality from class File and module FileTest, +# along with some functionality from class Dir and module FileUtils. +# +# Here's an example string path and corresponding \Pathname object: +# +# path = 'lib/fileutils.rb' +# pn = Pathname.new(path) # => #<Pathname:lib/fileutils.rb> +# +# Each of these method pairs (\Pathname vs. \File) gives exactly the same result: +# +# pn.size # => 83777 +# File.size(path) # => 83777 +# +# pn.directory? # => false +# File.directory?(path) # => false +# +# pn.read.size # => 81074 +# File.read(path).size# # => 81074 +# +# Each of these method pairs gives similar results, +# but each \Pathname method returns a more versatile \Pathname object, +# instead of a string: +# +# pn.dirname # => #<Pathname:lib> +# File.dirname(path) # => "lib" +# +# pn.basename # => #<Pathname:fileutils.rb> +# File.basename(path) # => "fileutils.rb" +# +# pn.split # => [#<Pathname:lib>, #<Pathname:fileutils.rb>] +# File.split(path) # => ["lib", "fileutils.rb"] +# +# Each of these methods takes a block: +# +# pn.open do |file| +# p file +# end +# File.open(path) do |file| +# p file +# end +# +# The outputs for each: +# +# #<File:lib/fileutils.rb (closed)> +# #<File:lib/fileutils.rb (closed)> +# +# Each of these methods takes a block: +# +# pn.each_line do |line| +# p line +# break +# end +# File.foreach(path) do |line| +# p line +# break +# end +# +# The outputs for each: +# +# "# frozen_string_literal: true\n" +# "# frozen_string_literal: true\n" +# +# == More Methods +# +# Here is a sampling of other available methods: +# +# p1 = Pathname.new('/usr/lib') # => #<Pathname:/usr/lib> +# p1.absolute? # => true +# p2 = p1 + 'ruby/4.0' # => #<Pathname:/usr/lib/ruby/4.0> +# p3 = p1.parent # => #<Pathname:/usr> +# p4 = p2.relative_path_from(p3) # => #<Pathname:lib/ruby/4.0> +# p4.absolute? # => false +# p5 = Pathname.new('.') # => #<Pathname:.> +# p6 = p5 + 'usr/../var' # => #<Pathname:usr/../var> +# p6.cleanpath # => #<Pathname:var> +# p6.realpath # => #<Pathname:/var> +# p6.children.take(2) +# # => [#<Pathname:usr/../var/local>, #<Pathname:usr/../var/spool>] +# +# == Breakdown of functionality +# +# === Core methods +# +# These methods are effectively manipulating a String, because that's +# all a path is. None of these access the file system except for +# #mountpoint?, #children, #each_child, #realdirpath and #realpath. +# +# - + +# - #join +# - #parent +# - #root? +# - #absolute? +# - #relative? +# - #relative_path_from +# - #each_filename +# - #cleanpath +# - #realpath +# - #realdirpath +# - #children +# - #each_child +# - #mountpoint? +# +# === File status predicate methods +# +# These methods are a facade for FileTest: +# - #blockdev? +# - #chardev? +# - #directory? +# - #executable? +# - #executable_real? +# - #exist? +# - #file? +# - #grpowned? +# - #owned? +# - #pipe? +# - #readable? +# - #world_readable? +# - #readable_real? +# - #setgid? +# - #setuid? +# - #size +# - #size? +# - #socket? +# - #sticky? +# - #symlink? +# - #writable? +# - #world_writable? +# - #writable_real? +# - #zero? +# +# === File property and manipulation methods +# +# These methods are a facade for File: +# - #each_line(*args, &block) +# - #read(*args) +# - #binread(*args) +# - #readlines(*args) +# - #sysopen(*args) +# - #write(*args) +# - #binwrite(*args) +# - #atime +# - #birthtime +# - #ctime +# - #mtime +# - #chmod(mode) +# - #lchmod(mode) +# - #chown(owner, group) +# - #lchown(owner, group) +# - #fnmatch(pattern, *args) +# - #fnmatch?(pattern, *args) +# - #ftype +# - #make_link(old) +# - #open(*args, &block) +# - #readlink +# - #rename(to) +# - #stat +# - #lstat +# - #make_symlink(old) +# - #truncate(length) +# - #utime(atime, mtime) +# - #lutime(atime, mtime) +# - #basename(*args) +# - #dirname +# - #extname +# - #expand_path(*args) +# - #split +# +# === Directory methods +# +# These methods are a facade for Dir: +# - Pathname.glob(*args) +# - Pathname.getwd / Pathname.pwd +# - #rmdir +# - #entries +# - #each_entry(&block) +# - #mkdir(*args) +# - #opendir(*args) +# +# === Utilities +# +# These methods are a mixture of Find, FileUtils, and others: +# - #find(&block) +# - #mkpath +# - #rmtree +# - #unlink / #delete +# +# +# == 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. +# +class Pathname + + # The version string. + VERSION = "0.4.0" + + # :stopdoc: + + attr_reader :path + protected :path + + # :startdoc: + + # call-seq: + # Pathname.new(path) -> new_pathname + # + # Returns a new \Pathname object based on the given +path+, + # via <tt>File.path(path).dup</tt>. + # the +path+ may be a String, a File, a Dir, or another \Pathname; + # see File.path: + # + # Pathname.new('.') # => #<Pathname:.> + # Pathname.new('/usr/bin') # => #<Pathname:/usr/bin> + # Pathname.new(File.new('LEGAL')) # => #<Pathname:LEGAL> + # Pathname.new(Dir.new('.')) # => #<Pathname:.> + # Pathname.new(Pathname.new('.')) # => #<Pathname:.> + # + def initialize(path) + @path = File.path(path).dup + rescue TypeError => e + raise e.class, "Pathname.new requires a String, #to_path or #to_str", cause: nil + end + + # + # Freze self. + # + def freeze + super + @path.freeze + self + end + + # call-seq: + # self == other -> true or false + # + # Returns whether the stored paths in +self+ and +other+ are equal: + # + # pn = Pathname('lib') + # pn == Pathname('lib') # => true + # pn == Pathname('./lib') # => false + # + # Returns +false+ if +other+ is not a pathname: + # + # pn == 'lib' # => false + # + def ==(other) + return false unless Pathname === other + other.path == @path + end + alias === == + alias eql? == + + def hash # :nodoc: + @path.hash + end + + # Return the path as a String. + def to_s + @path.dup + end + + # to_path is implemented so Pathname objects are usable with File.open, etc. + alias to_path to_s + + def inspect # :nodoc: + "#<#{self.class}:#{@path}>" + end + + # Creates a full path, including any intermediate directories that don't yet + # exist. + # + # See FileUtils.mkpath and FileUtils.mkdir_p + def mkpath(mode: nil) + path = @path == '/' ? @path : @path.chomp('/') + + stack = [] + until File.directory?(path) || (parent = File.dirname(path)) == path + stack.push path + path = parent + end + + stack.reverse_each do |dir| + dir = dir == '/' ? dir : dir.chomp('/') + if mode + Dir.mkdir dir, mode + File.chmod mode, dir + else + Dir.mkdir dir + end + rescue SystemCallError + raise unless File.directory?(dir) + end + + self + end + + def prepend_prefix(prefix, relpath) # :nodoc: + if relpath.empty? + File.dirname(prefix) + elsif has_separator?(prefix) + add_trailing_separator(File.dirname(prefix)) + relpath + else + prefix + relpath + end + end + private :prepend_prefix + + # :markup: markdown + # + # call-seq: + # cleanpath(symlinks = false) -> new_pathname + # + # Returns a new \Pathname object, "cleaned" of unnecessary separators, + # single-dot entries, and double-dot entries. + # + # When `self` is empty, returns a pathname with a single-dot entry: + # + # ``` + # Pathname('').cleanpath # => #<Pathname:.> + # ``` + # + # <b>Separators</b> + # + # A lone separator is preserved: + # + # ``` + # Pathname('/').cleanpath # => #<Pathname:/> + # ``` + # + # Multiple trailing separators are removed: + # + # ``` + # Pathname('foo/////').cleanpath # => #<Pathname:foo> + # Pathname('foo/').cleanpath # => #<Pathname:foo> + # ``` + # + # Multiple embedded separators are reduced to a single separator: + # + # ``` + # Pathname('foo///bar').cleanpath # => #<Pathname:foo/bar> + # ``` + # + # Multiple leading separators are reduced: + # + # ``` + # # On Windows, where File.dirname('//') == '//'. + # Pathname('/////foo').cleanpath # => #<Pathname://foo> + # Pathname('/////').cleanpath # => #<Pathname://> + # # Otherwise, where File.dirname('//') == '/'. + # Pathname('/////foo').cleanpath # => #<Pathname:/foo> + # Pathname('/////').cleanpath # => #<Pathname:/> + # ``` + # + # <b>Single-Dot Entries</b> + # + # A lone single-dot entry is preserved: + # + # ``` + # Pathname('.').cleanpath # => #<Pathname:.> + # ``` + # + # A non-lone single-dot entry, regardless of its location, is removed: + # + # ``` + # Pathname('foo/././././bar').cleanpath # => #<Pathname:foo/bar> + # Pathname('./foo/./././bar').cleanpath # => #<Pathname:foo/bar> + # Pathname('foo/./././bar/./').cleanpath # => #<Pathname:foo/bar> + # ``` + # + # <b>Double-Dot Entries</b> + # + # A lone double-dot entry is preserved: + # + # ``` + # Pathname('..').cleanpath # => #<Pathname:..> + # ``` + # + # When a non-lone double-dot entry is preceded by a named entry, both are removed: + # + # ``` + # Pathname('foo/..').cleanpath # => #<Pathname:.> + # Pathname('foo/../bar').cleanpath # => #<Pathname:bar> + # Pathname('foo/../bar/..').cleanpath # => #<Pathname:.> + # Pathname('foo/bar/./../..').cleanpath # => #<Pathname:.> + # ``` + # + # When a non-lone double-dot entry is _not_ preceded by a named entry, + # it is preserved: + # + # ``` + # Pathname('../..').cleanpath # => #<Pathname:../..> + # ``` + # + # A non-lone meaningless double-dot entry is removed: + # + # ``` + # Pathname('/..').cleanpath # => #<Pathname:/> + # Pathname('/../..').cleanpath # => #<Pathname:/> + # ``` + # + # <b> Symbolic Links</b> + # + # If the path may contain [symbolic links][symbolic link], + # consider give optional argument `symlinks` as `true`; + # the method then uses a more conservative algorithm + # that avoids breaking symbolic links. + # This may preserve more double-dot entries than are absolutely necessary, + # but without accessing the filesystem, this can't be avoided. + # + # Examples: + # + # ``` + # Pathname('a/').cleanpath # => #<Pathname:a> + # Pathname('a/').cleanpath(true) # => #<Pathname:a/> + # + # Pathname('a/.').cleanpath # => #<Pathname:a> + # Pathname('a/.').cleanpath(true) # => #<Pathname:a/.> + # + # Pathname('a/./').cleanpath # => #<Pathname:a> + # Pathname('a/./').cleanpath(true) # => #<Pathname:a/.> + # + # Pathname('a/b/.').cleanpath # => #<Pathname:a/b> + # Pathname('a/b/.').cleanpath(true) # => #<Pathname:a/b/.> + # + # Pathname('a/../.').cleanpath # => #<Pathname:.> + # Pathname('a/../.').cleanpath(true) # => #<Pathname:a/..> + # + # Pathname('a/b/../../../../c/../d').cleanpath + # # => #<Pathname:../../d> + # Pathname('a/b/../../../../c/../d').cleanpath(true) + # # => #<Pathname:a/b/../../../../c/../d> + # ``` + # + # [symbolic link]: https://en.wikipedia.org/wiki/Symbolic_link + # + def cleanpath(consider_symlink=false) + if consider_symlink + cleanpath_conservative + else + cleanpath_aggressive + end + end + + # + # Clean the path simply by resolving and removing excess +.+ and +..+ entries. + # Nothing more, nothing less. + # + def cleanpath_aggressive # :nodoc: + path = @path + names = [] + pre = path + while r = chop_basename(pre) + pre, base = r + case base + when '.' + when '..' + names.unshift base + else + if names[0] == '..' + names.shift + else + names.unshift base + end + end + end + pre.tr!(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR + if has_separator?(File.basename(pre)) + names.shift while names[0] == '..' + end + self.class.new(prepend_prefix(pre, File.join(*names))) + end + private :cleanpath_aggressive + + def cleanpath_conservative # :nodoc: + path = @path + names = [] + pre = path + while r = chop_basename(pre) + pre, base = r + names.unshift base if base != '.' + end + pre.tr!(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR + if has_separator?(File.basename(pre)) + names.shift while names[0] == '..' + end + if names.empty? + self.class.new(File.dirname(pre)) + else + if names.last != '..' && File.basename(path) == '.' + names << '.' + end + result = prepend_prefix(pre, File.join(*names)) + if /\A(?:\.|\.\.)\z/ !~ names.last && has_trailing_separator?(path) + self.class.new(add_trailing_separator(result)) + else + self.class.new(result) + end + end + end + private :cleanpath_conservative + + # Returns the parent directory. + # + # This is same as <code>self + '..'</code>. + def parent + self + '..' + end + + # Returns +true+ if +self+ points to a mountpoint. + def mountpoint? + begin + stat1 = self.lstat + stat2 = self.parent.lstat + stat1.dev != stat2.dev || stat1.ino == stat2.ino + rescue Errno::ENOENT + false + end + end + + # The opposite of Pathname#absolute? + # + # It returns +false+ if the pathname begins with a slash. + # + # p = Pathname.new('/im/sure') + # p.relative? + # #=> false + # + # p = Pathname.new('not/so/sure') + # p.relative? + # #=> true + def relative? + !absolute? + end + + # :markup: markdown + # + # call-seq: + # each_filename {|component| ... } -> nil + # each_filename -> new_enumerator + # + # With a block given, yields each component of the string path: + # + # ```ruby + # Pathname('/foo/bar/baz').each_filename {|filename| p filename } + # => nil + # ``` + # + # Output: + # + # ```text + # "foo" + # "bar" + # "baz" + # ``` + # + # With no block given, returns a new Enumerator. + def each_filename # :yield: filename + return to_enum(__method__) unless block_given? + _, names = split_names(@path) + names.each {|filename| yield filename } + nil + end + + # :markup: markdown + # + # call-seq: + # descend {|entry| ... } -> nil + # descend -> new_enumerator + # + # With a block given, yields a new pathname for each successive dirname + # in the stored path; see File.dirname: + # + # ```ruby + # # Absolute path. + # Pathname('/path/to/some/file.rb').descend {|pn| p pn } + # # #<Pathname:/> + # # #<Pathname:/path> + # # #<Pathname:/path/to> + # # #<Pathname:/path/to/some> + # # #<Pathname:/path/to/some/file.rb> + # # Relative path. + # Pathname('path/to/some/file.rb').descend {|pn| p pn } + # # #<Pathname:path> + # # #<Pathname:path/to> + # # #<Pathname:path/to/some> + # # #<Pathname:path/to/some/file.rb> + # ``` + # + # With no block given, returns a new Enumerator. + def descend + return to_enum(__method__) unless block_given? + vs = [] + ascend {|v| vs << v } + vs.reverse_each {|v| yield v } + nil + end + + # call-seq: + # ascend {|entry| ... } -> nil + # ascend -> new_enumerator + # + # With a block given, + # yields +self+, then a new pathname for each successive dirname in the stored path; + # see File.dirname: + # + # Pathname('/path/to/some/file.rb').ascend {|dirname| p dirname} + # #<Pathname:/path/to/some/file.rb> + # #<Pathname:/path/to/some> + # #<Pathname:/path/to> + # #<Pathname:/path> + # #<Pathname:/> + # + # With no block given, returns a new Enumerator. + def ascend + return to_enum(__method__) unless block_given? + path = @path + yield self + while r = chop_basename(path) + path, = r + break if path.empty? + yield self.class.new(del_trailing_separator(path)) + end + end + + # call-seq: + # self + other -> new_pathname + # + # Returns a new \Pathname object based on the content of +self+ and +other+; + # argument +other+ may be a String, a File, a Dir, or another \Pathname: + # + # pn = Pathname('foo') # => #<Pathname:foo> + # pn + 'bar' # => #<Pathname:foo/bar> + # pn + File.new('LEGAL') # => #<Pathname:foo/LEGAL> + # pn + Dir.new('lib') # => #<Pathname:foo/lib> + # pn + Pathname('bar') # => #<Pathname:foo/bar> + # + # When +other+ specifies a relative path (see #relative?), + # it is combined with +self+ to form a new pathname: + # + # Pathname('/a/b') + 'c' # => #<Pathname:/a/b/c> + # + # Extra component separators (<tt>'/'</tt>) are removed: + # + # Pathname('/a/b/') + 'c' # => #<Pathname:/a/b/c> + # + # Extra current-directory components (<tt>'.'</tt>) are removed: + # + # Pathname('a') + '.' # => #<Pathname:a> + # Pathname('.') + 'a' # => #<Pathname:a> + # Pathname('.') + '.' # => #<Pathname:.> + # + # Parent-directory components (<tt>'..'</tt>) are: + # + # - Resolved, when possible: + # + # Pathname('a') + '..' # => #<Pathname:.> + # Pathname('a/b') + '..' # => #<Pathname:a> + # Pathname('/') + '../a' # => #<Pathname:/a> + # Pathname('a') + '../b' # => #<Pathname:b> + # Pathname('a/b') + '../c' # => #<Pathname:a/c> + # Pathname('a//b/c') + '../d//e' # => #<Pathname:a//b/d//e> + # + # - Removed, when not needed: + # + # Pathname('/') + '..' # => #<Pathname:/> + # + # - Retained, when needed: + # + # Pathname('..') + '..' # => #<Pathname:../..> + # Pathname('..') + '../a' # => #<Pathname:../../a> + # + # When +other+ specifies an absolute path (see #absolute?), + # equivalent to <tt>Pathname(other.to_s)</tt>: + # + # Pathname('/a') + '/b/c' # => #<Pathname:/b/c> + # + # Occurrences of <tt>'/'</tt>, <tt>'.'</tt>, and <tt>'..'</tt> are preserved: + # + # Pathname('/a') + '//b//c/./../d' # => #<Pathname://b//c/./../d> + # + # This method does not access the file system, so +other+ need not represent + # an existing (or even a valid) file or directory path: + # + # Pathname('/var') + 'nosuch:ever' # => #<Pathname:/var/nosuch:ever> + # + def +(other) + other = Pathname.new(other) unless Pathname === other + Pathname.new(plus(@path, other.path)) + end + alias / + + + # (path1, path2) -> path + def plus(path1, path2) # :nodoc: + prefix2 = path2 + index_list2 = [] + basename_list2 = [] + while r2 = chop_basename(prefix2) + prefix2, basename2 = r2 + index_list2.unshift prefix2.length + basename_list2.unshift basename2 + end + return path2 if prefix2 != '' + prefix1 = path1 + while true + while !basename_list2.empty? && basename_list2.first == '.' + index_list2.shift + basename_list2.shift + end + break unless r1 = chop_basename(prefix1) + prefix1, basename1 = r1 + next if basename1 == '.' + if basename1 == '..' || basename_list2.empty? || basename_list2.first != '..' + prefix1 = prefix1 + basename1 + break + end + index_list2.shift + basename_list2.shift + end + r1 = chop_basename(prefix1) + if !r1 && (r1 = has_separator?(File.basename(prefix1))) + while !basename_list2.empty? && basename_list2.first == '..' + index_list2.shift + basename_list2.shift + end + end + if !basename_list2.empty? + suffix2 = path2[index_list2.first..-1] + r1 ? File.join(prefix1, suffix2) : prefix1 + suffix2 + else + r1 ? prefix1 : File.dirname(prefix1) + end + end + private :plus + + # + # Joins the given pathnames onto +self+ to create a new Pathname object. + # This is effectively the same as using Pathname#+ to append +self+ and + # all arguments sequentially. + # + # path0 = Pathname.new("/usr") # Pathname:/usr + # path0 = path0.join("bin/ruby") # Pathname:/usr/bin/ruby + # # is the same as + # path1 = Pathname.new("/usr") + "bin/ruby" # Pathname:/usr/bin/ruby + # path0 == path1 + # #=> true + # + def join(*args) + return self if args.empty? + result = args.pop + result = Pathname.new(result) unless Pathname === result + return result if result.absolute? + args.reverse_each {|arg| + arg = Pathname.new(arg) unless Pathname === arg + result = arg + result + return result if result.absolute? + } + self + result + end + + # :markup: markdown + # + # call-seq: + # children(with_dirnames = true) -> array_of_pathnames + # + # Returns an array of pathnames; + # each represents a child of the entry represented by `self`, + # which must be an existing directory in the underlying file system. + # + # With `with_dirnames` given as `true` (the default), + # each pathname contains the full entry: + # + # ```ruby + # Pathname('lib').children.size # => 72 + # Pathname('lib').children.take(3) + # # => [#<Pathname:lib/bundled_gems.rb>, #<Pathname:lib/bundler>, #<Pathname:lib/bundler.rb>] + # ``` + # With `with_dirnames` given as `false`, + # each pathname contains only the basename of the entry: + # + # ```ruby + # Pathname('lib').children(false).take(3) + # # => [#<Pathname:bundled_gems.rb>, #<Pathname:bundler>, #<Pathname:bundler.rb>] + # ``` + # + # Note that entries `.` and `..` in directory are not actually children, + # and so are never included in the result. + def children(with_directory=true) + with_directory = false if @path == '.' + result = Dir.children(@path) + if with_directory + result.map! {|e| self.class.new(File.join(@path, e))} + else + result.map! {|e| self.class.new(e)} + end + result + end + + # :markup: markdown + # + # call-seq: + # each_child(with_dirnames = true) {|entry| ... } -> array_of_pathnames + # each_child(with_dirnames = true) -> new_enumerator + # + # With a block given and `with_dirnames` given as `true` (the default), + # yields a new pathname for each child + # of the entry represented by `self`; + # returns an array of those pathnames: + # + # ```ruby + # Pathname('include').each_child {|child| p child } + # # #<Pathname:include/ruby> + # # #<Pathname:include/ruby.h> + # # => [#<Pathname:include/ruby>, #<Pathname:include/ruby.h>] + # ``` + # + # With a block given and `with_dirnames` given as `false`, + # yields a new pathname for each child + # of the entry represented by `self` with its dirname omitted; + # returns an array of those pathnames: + # + # ```ruby + # Pathname('include').each_child(false) {|child| p child } + # # #<Pathname:ruby> + # # #<Pathname:ruby.h> + # # => [#<Pathname:ruby>, #<Pathname:ruby.h>] + # ``` + # + # Note that entries `'.'` and `'..'` are not children. + # + # With no block given, returns a new Enumerator. + def each_child(with_directory=true, &b) + children(with_directory).each(&b) + end + + # + # Returns a relative path from the given +base_directory+ to the receiver. + # + # If +self+ is absolute, then +base_directory+ must be absolute too. + # + # If +self+ is relative, then +base_directory+ must be relative too. + # + # This method doesn't access the filesystem. It assumes no symlinks. + # + # ArgumentError is raised when it cannot find a relative path. + # + # Note that this method does not handle situations where the case sensitivity + # of the filesystem in use differs from the operating system default. + # + def relative_path_from(base_directory) + base_directory = Pathname.new(base_directory) unless base_directory.is_a? Pathname + dest_directory = self.cleanpath.path + base_directory = base_directory.cleanpath.path + dest_prefix = dest_directory + dest_names = [] + while r = chop_basename(dest_prefix) + dest_prefix, basename = r + dest_names.unshift basename if basename != '.' + end + base_prefix = base_directory + base_names = [] + while r = chop_basename(base_prefix) + base_prefix, basename = r + base_names.unshift basename if basename != '.' + end + unless same_paths?(dest_prefix, base_prefix) + raise ArgumentError, "different prefix: #{dest_prefix.inspect} and #{base_directory.inspect}" + end + while !dest_names.empty? && + !base_names.empty? && + same_paths?(dest_names.first, base_names.first) + dest_names.shift + base_names.shift + end + if base_names.include? '..' + raise ArgumentError, "base_directory has ..: #{base_directory.inspect}" + end + base_names.fill('..') + relpath_names = base_names + dest_names + if relpath_names.empty? + Pathname.new('.') + else + Pathname.new(File.join(*relpath_names)) + end + end +end + +class Pathname # * File * + + # :markup: markdown + # + # call-seq: + # each_line(sep = $/, **opts) {|line| ... } → nil + # each_line(limit, **opts) {|line| ... } → nil + # each_line(sep, limit, **opts) {|line| ... } → nil + # each_line(...) → new_enumerator + # + # With a block given, calls the block with each line + # from the file represented by `self`; + # returns `nil`: + # + # ```ruby + # lines = [] + # Pathname('COPYING').each_line {|line| lines << line } + # lines.take(3) + # # => + # # ["{日本語}[rdoc-ref:COPYING.ja]\n", + # # "\n", + # # "Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.\n"] + # ``` + # + # The lines are read using IO.foreach, + # all arguments and options are passed to that method; + # see details at IO.foreach. + # + # With no block given, returns a new Enumerator. + def each_line(...) # :yield: line + File.foreach(@path, ...) + end + + # call-seq: + # read(length = nil, offset = 0, **opts) -> string or nil + # + # Reads and returns some or all of the content of the file + # whose path is <tt>self.to_s</tt>. + # + # With no arguments given, + # reads in text mode and returns the entire content of the file: + # + # Pathname.new('t.txt').read + # # => "First line\nSecond line\n\nFourth line\nFifth line\n" + # Pathname.new('t.ja').read + # # => "こんにちは" + # Pathname.new('t.dat').read + # # => "\xFE\xFF\x99\x90\x99\x91\x99\x92\x99\x93\x99\x94" + # + # On Windows, text mode can terminate reading and leave bytes in the file unread + # when encountering certain special bytes. + # Consider using #binread if all bytes in the file should be read. + # + # With argument +length+ given, returns +length+ bytes if available: + # + # Pathname.new('t.txt').read(7) + # # => "First l" + # Pathname.new('t.ja').read(7) + # # => "\xE3\x81\x93\xE3\x82\x93\xE3" + # Pathname.new('t.dat').read(7) + # # => "\xFE\xFF\x99\x90\x99\x91\x99" + # + # Returns all bytes if +length+ is larger than the files size: + # + # Pathname.new('t.txt').read(700) + # # => "First line\r\nSecond line\r\n\r\nFourth line\r\nFifth line\r\n" + # Pathname.new('t.ja').read(700) + # # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" + # Pathname.new('t.dat').read(700) + # # => "\xFE\xFF\x99\x90\x99\x91\x99\x92\x99\x93\x99\x94" + # + # With arguments +length+ and +offset+ given, + # returns +length+ bytes if available, beginning at the given +offset+: + # + # Pathname.new('t.txt').read(10, 2) + # # => "rst line\r\n" + # Pathname.new('t.ja').read(10, 2) + # # => "\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1" + # Pathname.new('t.dat').read(10, 2) + # # => "\x99\x90\x99\x91\x99\x92\x99\x93\x99\x94" + # + # Returns +nil+ if +offset+ is past the end of the file: + # + # Pathname.new('t.txt').read(10, 200) # => nil + # + # Optional keyword arguments +opts+ specify: + # + # - {Open Options}[rdoc-ref:IO@Open+Options]. + # - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. + # + def read(...) File.read(@path, ...) end + + # call-seq: + # binread(length = nil, offset = 0) -> string or nil + # + # Behaves like #read, except that the file is opened in binary mode + # with ASCII-8BIT encoding. + # + def binread(...) File.binread(@path, ...) end + + # See <tt>File.readlines</tt>. Returns all the lines from the file. + def readlines(...) File.readlines(@path, ...) end + + # See <tt>File.sysopen</tt>. + def sysopen(...) File.sysopen(@path, ...) end + + # call-seq: + # write(data, offset = 0, **opts) -> nonnegative_integer + # + # Opens the file at +self.to_s+, writes the given +data+ to it, + # and closes the file; returns the number of bytes written. + # + # With only argument +data+ given, writes the given data to the file: + # + # path = 't.tmp' + # pn = Pathname.new(path) + # pn.write('foo') # => 3 + # File.read(path) # => "foo" + # + # If +offset+ is zero (the default), the file is overwritten: + # + # pn.write('bar') + # File.read(path) # => "bar" + # + # If +offset+ in within the file content, the file is partly overwritten: + # + # pn.write('foobarbaz') + # pn.write('BAR', 3) + # File.read(path) # => "fooBARbaz" + # + # If +offset+ is outside the file content, + # the file is padded with null characters <tt>"\u0000"</tt>: + # + # pn.write('bat', 12) + # File.read(path) # => "fooBARbaz\u0000\u0000\u0000bat" + # + # Optional keyword arguments +opts+ specify: + # + # - {Open Options}[rdoc-ref:IO@Open+Options]. + # - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. + # + def write(...) File.write(@path, ...) end + + # call-seq: + # binwrite(string, offset = 0, **opts) -> nonnegative_integer + # + # Behaves like #write, except that the file is opened in binary mode + # with ASCII-8BIT encoding. + def binwrite(...) File.binwrite(@path, ...) end + + # call-seq: + # atime -> new_time + # + # Returns a new Time object containing the time of the most recent + # access (read or write) to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: + # + # # Work in a temporary directory. + # require 'tmpdir' + # Dir.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # Dir.mkdir(dirpath) + # dir_pn = Pathname(dirpath) + # puts "Create directory; establishes atime for directory." + # puts " Directory atime: #{dir_pn.atime}" + # sleep(1) + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # puts "Create file; establishes atime for file, updates atime for directory." + # File.write(filepath, 'foo') + # file_pn = Pathname(filepath) + # puts " File atime: #{file_pn.atime}" + # puts " Directory atime: #{dir_pn.atime}" + # sleep(1) + # puts "Write file; updates atimes for file and directory." + # File.write(filepath, 'bar') + # puts " File atime: #{file_pn.atime}" + # puts " Directory atime: #{dir_pn.atime}" + # end + # + # Output: + # + # Create directory; establishes atime for directory. + # Directory atime: 2026-05-14 14:36:43 +0100 + # Create file; establishes atime for file, updates atime for directory. + # File atime: 2026-05-14 14:36:44 +0100 + # Directory atime: 2026-05-14 14:36:44 +0100 + # Write file; updates atimes for file and directory. + # File atime: 2026-05-14 14:36:45 +0100 + # Directory atime: 2026-05-14 14:36:45 +0100 + # + def atime() File.atime(@path) end + + # :markup: markdown + # + # call-seq: + # birthtime -> new_time + # + # Returns a new Time object containing the create time of the entry + # represented by `self`; + # see [File System Timestamps](rdoc-ref:file/timestamps.md): + # + # ```ruby + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # puts "Create directory; directory birthtime established." + # dir_pn.mkdir + # puts " Directory birthtime: #{dir_pn.birthtime}" + # sleep(1) + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # puts "Create file; file birthtime established; directory birthtime not updated." + # file_pn.write('foo') + # puts " File birthtime: #{file_pn.birthtime}" + # puts " Directory birthtime: #{dir_pn.birthtime}" + # sleep(1) + # puts "Write file; neither birthtime updated." + # file_pn.write('bar') + # puts " File birthtime: #{file_pn.birthtime}" + # puts " Directory birthtime: #{dir_pn.birthtime}" + # end + # ``` + # + # Output: + # + # ```text + # Create directory; directory birthtime established. + # Directory birthtime: 2026-05-14 23:41:12 +0100 + # Create file; file birthtime established; directory birthtime not updated. + # File birthtime: 2026-05-14 23:41:13 +0100 + # Directory birthtime: 2026-05-14 23:41:12 +0100 + # Write file; neither birthtime updated. + # File birthtime: 2026-05-14 23:41:13 +0100 + # Directory birthtime: 2026-05-14 23:41:12 +0100 + # ``` + # + def birthtime() File.birthtime(@path) end + + # :markup: markdown + # + # call-seq: + # ctime -> new_time + # + # On Windows, returns the #birthtime. + # + # On other systems, + # returns a new Time object containing the time of the most recent + # metadata change to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: + # + # ```ruby + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # puts "Create directory; directory ctime established." + # dir_pn.mkdir + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # puts "Create file; file ctime established; directory ctime updated." + # file_pn.write('foo') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Write file; file ctime updated; directory ctime not updated." + # file_pn.write('bar') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Read file; neither ctime not updated." + # file_pn.read + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # end + # ``` + # + # Output: + # + # ```text + # Create directory; directory ctime established. + # Directory ctime: 2026-05-20 14:05:05 -0500 + # Create file; file ctime established; directory ctime updated. + # File ctime: 2026-05-20 14:05:06 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Write file; file ctime updated; directory ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Read file; neither ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # ``` + # + def ctime() File.ctime(@path) end + + # See <tt>File.mtime</tt>. Returns last modification time. + def mtime() File.mtime(@path) end + + + # :markup: markdown + # + # call-seq: + # chmod(mode) -> 1 + # + # Changes the mode (i.e., permissions) of the entry represented by `self`; + # see {File Permissions}[rdoc-ref:File@File+Permissions]; + # returns `1`: + # + # ```ruby + # # A helper method to make an integer mode display as octal. + # def pretty(mode); '0' + (mode & 0777).to_s(8); end + # + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # dir_pn.mkdir + # # The directory mode. + # puts "Original directory mode: #{pretty(dir_pn.stat.mode)}" + # # Change the directory mode. + # dir_pn.chmod(0777) + # puts "New directory mode: #{pretty(dir_pn.stat.mode)}" + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # # Create the file. + # file_pn.write('foo') + # # The file mode. + # puts "Original file mode: #{pretty(file_pn.stat.mode)}" + # # Change the file modes. + # file_pn.chmod(0777) + # puts "New file mode: #{pretty(file_pn.stat.mode)}" + # end + # ``` + # + # Output: + # + # ```text + # Original directory mode: 0775 + # New directory mode: 0777 + # Original file mode: 0664 + # New file mode: 0777 + # ``` + # + def chmod(mode) File.chmod(mode, @path) end + + # See <tt>File.lchmod</tt>. + def lchmod(mode) File.lchmod(mode, @path) end + + # :markup: markdown + # + # call-seq: + # chown(owner_id, group_id) -> 0 + # + # Changes the owner and group of an entry (directory or file): + # + # ```ruby + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # dir_pn.mkdir + # dir_stat = File.stat(dirpath) + # puts "Original directory owner: #{dir_stat.uid}" + # puts "Original directory group: #{dir_stat.gid}" + # dir_pn.chown(1000, 1000) + # dir_stat = File.stat(dirpath) + # puts "New directory owner: #{dir_stat.uid}" + # puts "New directory group: #{dir_stat.gid}" + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # # Create the file. + # file_pn.write('foo') + # file_stat = File.stat(filepath) + # puts "Original file owner: #{file_stat.uid}" + # puts "Original file group: #{file_stat.gid}" + # file_pn = Pathname(dirpath) + # file_pn.chown(1000, 1000) + # file_stat = File.stat(dirpath) + # puts "New file owner: #{file_stat.uid}" + # puts "New file group: #{file_stat.gid}" + # end + # ``` + # + # Output: + # + # ```text + # Original directory owner: 0 + # Original directory group: 0 + # New directory owner: 1000 + # New directory group: 1000 + # Original file owner: 0 + # Original file group: 0 + # New file owner: 1000 + # New file group: 1000 + # ``` + # + # Notes: + # + # - On Windows, the owner and group are not changed. + # - Only a process with superuser privileges can change the owner of an entry. + # - The owner of an entry can change its group to any group + # to which the owner belongs. + # - A +nil+ or +-1+ owner or group id is ignored. + # - The method follows symbolic links to the target entry. + # + def chown(owner, group) File.chown(owner, group, @path) end + + # See <tt>File.lchown</tt>. + def lchown(owner, group) File.lchown(owner, group, @path) end + + # See <tt>File.fnmatch</tt>. Return +true+ if the receiver matches the given + # pattern. + def fnmatch(pattern, ...) File.fnmatch(pattern, @path, ...) end + + # See <tt>File.fnmatch?</tt> (same as #fnmatch). + def fnmatch?(pattern, ...) File.fnmatch?(pattern, @path, ...) end + + # See <tt>File.ftype</tt>. Returns "type" of file ("file", "directory", + # etc). + def ftype() File.ftype(@path) end + + # See <tt>File.link</tt>. Creates a hard link. + def make_link(old) File.link(old, @path) end + + # See <tt>File.open</tt>. Opens the file for reading or writing. + def open(...) # :yield: file + File.open(@path, ...) + end + + # See <tt>File.readlink</tt>. Read symbolic link. + def readlink() self.class.new(File.readlink(@path)) end + + # See <tt>File.rename</tt>. Rename the file. + def rename(to) File.rename(@path, to) end + + # See <tt>File.stat</tt>. Returns a <tt>File::Stat</tt> object. + def stat() File.stat(@path) end + + # See <tt>File.lstat</tt>. + def lstat() File.lstat(@path) end + + # See <tt>File.symlink</tt>. Creates a symbolic link. + def make_symlink(old) File.symlink(old, @path) end + + # See <tt>File.truncate</tt>. Truncate the file to +length+ bytes. + def truncate(length) File.truncate(@path, length) end + + # See <tt>File.utime</tt>. Update the access and modification times. + def utime(atime, mtime) File.utime(atime, mtime, @path) end + + # Update the access and modification times of the file. + # + # Same as Pathname#utime, but does not follow symbolic links. + # + # See File.lutime. + def lutime(atime, mtime) File.lutime(atime, mtime, @path) end + + # call-seq: + # basename(path, suffix = '') -> new_pathname + # + # Returns a new \Pathname object containing all or part of the last entry + # of the path represented by +self+. + # Entries are delimited by the value of constant File::SEPARATOR + # and, if non-nil, the value of constant File::ALT_SEPARATOR. + # + # When +suffix+ is the empty string <tt>''</tt>, returns all of the last entry: + # + # Pathname.new('foo/bar/baz/bat.txt').basename # => #<Pathname:bat.txt> + # Pathname.new('foo/bar/baz').basename # => #<Pathname:baz> + # + # File::SEPARATOR # => "/" + # Pathname.new('foo/bar.txt////').basename # => #<Pathname:bar.txt> + # File::ALT_SEPARATOR # => "\\" # On Windows. + # Pathname.new('foo/bar.txt//\\\\//').basename # => #<Pathname:bar.txt> + # + # When +suffix+ is <tt>'.*'</tt>, + # the last {filename extension}[https://en.wikipedia.org/wiki/Filename_extension], + # if any, is removed: + # + # Pathname.new('foo/bar.txt').basename('.*') # => #<Pathname:bar> + # Pathname.new('foo/bar.txt.old').basename('.*') # => #<Pathname:bar.txt> + # Pathname.new('foo/bar').basename('.*') # => #<Pathname:bar> + # + # When +suffix+ is any string other than <tt>''</tt> or <tt>'.*'</tt>, + # the matching trailing substring, if any, is removed: + # + # Pathname.new('foo/bar.txt').basename('.txt') # => #<Pathname:bar> + # Pathname.new('foo/bar.txt').basename('txt') # => #<Pathname:bar.> + # Pathname.new('foo/bar.txt').basename('*') # => #<Pathname:bar.txt> + # Pathname.new('foo/bar.txt').basename('.') # => #<Pathname:bar.txt> + # + def basename(...) self.class.new(File.basename(@path, ...)) end + + # See <tt>File.dirname</tt>. Returns all but the last component of the path. + def dirname() self.class.new(File.dirname(@path)) end + + # :markup: markdown + # + # call-seq: + # extname -> extension + # + # Returns the filename extension of `self` -- + # usually the portion of the string path beginning from the last period: + # + # ```ruby + # Pathname('t.rb').extname # => ".rb" + # Pathname('foo.bar.t.rb').extname # => ".rb" + # Pathname('foo/bar/t.rb').extname # => ".rb" + # Pathname('nosuch.txt').extname # => ".txt" # Path need not exist. + # ``` + # + # Returns the entire string when there is no period: + # + # ```ruby + # Pathname('foo').extname # => "" + # ``` + # + # Returns an empty string when the only period is the first character: + # + # ```ruby + # Pathname('.irbrc').extname # => "" + # ``` + # + # Returns an empty string or `'.'` when `path` ends with a period: + # + # ```ruby + # Pathname('foo.').extname # => "" # On Windows. + # Pathname('foo.').extname # => "." # Elsewhere. + # Pathname('foo....').extname # => "" # On Windows. + # Pathname('foo....').extname # => "." # Elsewhere. + # ``` + # + def extname() File.extname(@path) end + + # :markup: markdown + # + # call-seq: + # expand_path(dirpath = '.') -> new_pathname + # + # Returns a new pathname containing the absolute path for `self`. + # + # Evaluates a relative path with respect to the directory given by `dirpath`: + # + # ```ruby + # Dir.chdir('/snap') + # # Default dirpath. + # Pathname('README').expand_path # => #<Pathname:/snap/README> + # Pathname('bin').expand_path # => #<Pathname:/snap/bin> + # Pathname('bin/../var').expand_path # => #<Pathname:/snap/var> # Cleaned. + # # Other dirpath. + # Pathname('../zip').expand_path('/usr/bin/ruby') # => #<Pathname:/usr/bin/zip> + # Dir.chdir('/usr/bin') + # Pathname('../../snap').expand_path(__FILE__) # => #<Pathname:/usr/snap> + # ``` + # + # Evaluates an absolute path without respect to `dirpath`: + # + # ```ruby + # Pathname('/snap').expand_path # => #<Pathname:/snap> + # Pathname('/snap').expand_path.expand_path('nosuch') # => #<Pathname:/snap> + # Pathname('/snap/../snap').expand_path # => #<Pathname:/snap> # Cleaned. + # ``` + # + # More examples: + # + # ``` + # Dir.chdir('/usr/bin') + # Pathname('../../snap').expand_path(__FILE__) # => #<Pathname:/usr/snap> + # Pathname('../../snap').expand_path # => #<Pathname:/snap> + # ``` + # + def expand_path(...) self.class.new(File.expand_path(@path, ...)) end + + # See <tt>File.split</tt>. Returns the #dirname and the #basename in an + # Array. + def split() + array = File.split(@path) + raise TypeError, 'wrong argument type nil (expected Array)' unless Array === array + array.map {|f| self.class.new(f) } + end + + # Returns the real (absolute) pathname for +self+ in the actual filesystem. + # + # Does not contain symlinks or useless dots, +..+ and +.+. + # + # All components of the pathname must exist when this method is called. + def realpath(...) self.class.new(File.realpath(@path, ...)) end + + # Returns the real (absolute) pathname of +self+ in the actual filesystem. + # + # Does not contain symlinks or useless dots, +..+ and +.+. + # + # The last component of the real pathname can be nonexistent. + def realdirpath(...) self.class.new(File.realdirpath(@path, ...)) end +end + + +class Pathname # * FileTest * + + # :markup: markdown + # + # call-seq: + # blockdev? => true or false + # + # Returns whether `self` represents a path to a block device + # (i.e., a direct-access device): + # + # ```ruby + # Pathname('/dev/nvme0n1').blockdev? # => true + # Pathname('/dev/loop0').blockdev? # => true + # Pathname('/dev/tty').blockdev? # => false + # Pathname('/dev/null').blockdev? # => false + # Pathname('nosuch').blockdev? # => false + # Pathname($stdin).blockdev? # => false + # ``` + # + # The returned value is OS-dependent; on Windows, almost always `false`. + def blockdev?() FileTest.blockdev?(@path) end + + # :markup: markdown + # + # call-seq: + # chardev? => true or false + # + # Returns whether `self` represents a path to a character device + # (i.e., a sequential-access device): + # + # ```ruby + # Pathname('/dev/tty').chardev? # => true + # Pathname('/dev/null').chardev? # => true + # Pathname('/dev/nvme0n1').chardev? # => false + # Pathname('/dev/loop0').chardev? # => false + # Pathname($stdin).chardev? # => false + # Pathname('nosuch').chardev? # => false + # ``` + # + # The returned value is OS-dependent; on Windows, almost always `false`. + def chardev?() FileTest.chardev?(@path) end + + # :markup: markdown + # + # call-seq: + # empty? -> true or false + # + # Returns whether the entry represented by `self` exists and is empty: + # + # ```ruby + # dir_pn = Pathname('example_dir') + # dir_pn.empty? # => false # Dir does not exist. + # dir_pn.mkdir + # dir_pn.empty? # => true # Dir exists and is empty. + # + # file_pn = Pathname('example_dir/example.txt') + # file_pn.empty? # => false # File does not exist. + # file_pn.write('') + # file_pn.empty? # => true # File exists and is empty. + # dir_pn.empty? # => false # Dir exists and is not empty. + # file_pn.write('foo') + # file_pn.empty? # => false # File exists and is not empty. + # + # file_pn.delete + # dir_pn.delete + # ``` + # + def empty? + if FileTest.directory?(@path) + Dir.empty?(@path) + else + File.empty?(@path) + end + end + + # :markup: markdown + # + # call-seq: + # executable? -> true or false + # + # Returns whether the entry represented by `self` is executable; + # calls FileTest.executable? with argument `self.to_s`: + # + # ```ruby + # Pathname('bin/gem').executable? # => true + # Pathname('README.md').executable? # => false + # ``` + # + def executable?() FileTest.executable?(@path) end + + # :markup: markdown + # + # call-seq: + # executable_real? -> true or false + # + # Returns whether the entry represented by `self` is executable + # by the real user and group id of the current process; + # calls FileTest.executable_real? with argument `self.to_s`: + # + # ```ruby + # pn = Pathname('example') + # pn.write('') + # pn.executable_real? # => false + # pn.chmod(0100) + # pn.executable_real? # => true + # ``` + # + def executable_real?() FileTest.executable_real?(@path) end + + # :markup: markdown + # + # call-seq: + # exist? -> true or false + # + # Returns whether the entry represented by `self` exists: + # + # ```ruby + # Pathname('.').exist? # => true + # Pathname('README.md').exist? # => true + # Pathname('nosuch').exist? # => false + # ``` + # + def exist?() FileTest.exist?(@path) end + + # See <tt>FileTest.grpowned?</tt>. + def grpowned?() FileTest.grpowned?(@path) end + + # :markup: markdown + # + # call-seq: + # directory? -> true or false + # + # Returns whether the entry represented by `self` is a directory: + # + # ```ruby + # Pathname('/etc').directory? # => true + # Pathname('lib').directory? # => true + # Pathname('README.md').directory? # => false + # Pathname('nosuch').directory? # => false + # ``` + # + def directory?() FileTest.directory?(@path) end + + # See <tt>FileTest.file?</tt>. + def file?() FileTest.file?(@path) end + + # See <tt>FileTest.pipe?</tt>. + def pipe?() FileTest.pipe?(@path) end + + # See <tt>FileTest.socket?</tt>. + def socket?() FileTest.socket?(@path) end + + # See <tt>FileTest.owned?</tt>. + def owned?() FileTest.owned?(@path) end + + # See <tt>FileTest.readable?</tt>. + def readable?() FileTest.readable?(@path) end + + # See <tt>FileTest.world_readable?</tt>. + def world_readable?() File.world_readable?(@path) end + + # See <tt>FileTest.readable_real?</tt>. + def readable_real?() FileTest.readable_real?(@path) end + + # See <tt>FileTest.setuid?</tt>. + def setuid?() FileTest.setuid?(@path) end + + # See <tt>FileTest.setgid?</tt>. + def setgid?() FileTest.setgid?(@path) end + + # See <tt>FileTest.size</tt>. + def size() FileTest.size(@path) end + + # See <tt>FileTest.size?</tt>. + def size?() FileTest.size?(@path) end + + # See <tt>FileTest.sticky?</tt>. + def sticky?() FileTest.sticky?(@path) end + + # See <tt>FileTest.symlink?</tt>. + def symlink?() FileTest.symlink?(@path) end + + # See <tt>FileTest.writable?</tt>. + def writable?() FileTest.writable?(@path) end + + # See <tt>FileTest.world_writable?</tt>. + def world_writable?() File.world_writable?(@path) end + + # See <tt>FileTest.writable_real?</tt>. + def writable_real?() FileTest.writable_real?(@path) end + + # See <tt>FileTest.zero?</tt>. + def zero?() FileTest.zero?(@path) end +end + + +class Pathname # * Dir * + # call-seq: + # glob(patterns, **kwargs) → array_of_pathnames + # glob(patterns, **kwargs) {|pathname| ... } → nil + # + # Calls <tt>Dir.glob(patterns, **kwargs)</tt>, which yields or returns entry names; + # see Dir.glob. + # + # Required argument +patterns+ is a string pattern or an array of string patterns; + # note that these patterns are not regexps. + # + # Keyword arguments <tt>**kwargs</tt> are passed through to Dir.glob; + # see the documentation there. + # + # With no block given, returns an array of \Pathname objects; + # each is <tt>Pathname.new(entry_name)</tt> for an entry name returned by Dir.glob. + # + # Pathname.glob('*').take(3) + # # => [#<Pathname:BSDL>, #<Pathname:CONTRIBUTING.md>, #<Pathname:COPYING>] + # Pathname.glob(['o*', 'a*']).take(3) + # # => [#<Pathname:object.c>, #<Pathname:aclocal.m4>, #<Pathname:addr2line.c>] + # + # With a block given, calls the block with each pathname + # <tt>Pathname.new(entry_name)</tt>, + # where each +entry_name+ is a \Pathname object created by the value yielded by Dir.glob. + # + # a = [] + # Pathname.glob(['o*', 'a*']) {|pathname| a << pathname } + # a.take(3) + # # => [#<Pathname:object.c>, #<Pathname:aclocal.m4>, #<Pathname:addr2line.c>] + # + # Optional keyword argument +base+ is of particular interest. + # When it is given, its value specifies the base directory for the pathnames; + # each pattern string specifies entries relative to the base directory: + # + # Pathname.glob('*', base: 'lib').take(2) + # # => [#<Pathname:English.gemspec>, #<Pathname:English.rb>] + # Pathname.glob('*', base: 'lib/bundler').take(2) + # # => [#<Pathname:build_metadata.rb>, #<Pathname:bundler.gemspec>] + # + # Note that the base directory is not prepended to the entry names in the result. + def Pathname.glob(*args, **kwargs) # :yield: pathname + if block_given? + Dir.glob(*args, **kwargs) {|f| yield self.new(f) } + else + Dir.glob(*args, **kwargs).map {|f| self.new(f) } + end + end + + # Returns or yields Pathname objects. + # + # Pathname("ruby-2.4.2").glob("R*.md") + # #=> [#<Pathname:ruby-2.4.2/README.md>, #<Pathname:ruby-2.4.2/README.ja.md>] + # + # See Dir.glob. + # This method uses the +base+ keyword argument of Dir.glob. + def glob(*args, **kwargs) # :yield: pathname + if block_given? + Dir.glob(*args, **kwargs, base: @path) {|f| yield self + f } + else + Dir.glob(*args, **kwargs, base: @path).map {|f| self + f } + end + end + + # call-seq: + # Pathname.getwd -> new_pathname + # + # Returns a new \Pathname object containing the path to the current working directory + # (equivalent to <tt>Pathname.new(Dir.getwd)</tt>): + # + # Pathname.getwd # => #<Pathname:/home> + # + def Pathname.getwd() self.new(Dir.getwd) end + class << self + alias pwd getwd + end + + # :markup: markdown + # + # call-seq: + # entries -> array_of_pathnames + # + # Returns an array of pathnames, + # one for each entry in the directory represented by `self`: + # + # ```ruby + # Pathname('.').entries.take(5) + # # => + # # [#<Pathname:.>, + # # #<Pathname:..>, + # # #<Pathname:gc.rb>, + # # #<Pathname:yjit.rb>, + # # #<Pathname:iseq.h>] + # ``` + # + def entries() Dir.entries(@path).map {|f| self.class.new(f) } end + + # :markup: markdown + # + # call-seq: + # each_entry {|entry| ... } -> nil + # each_entry -> new_enumerator + # + # With a block given, + # yields a new pathname for each entry + # in the entry represented by `self`; + # returns `nil`: + # + # ```ruby + # Pathname('include').each_entry {|entry| p entry } + # # #<Pathname:ruby> + # # #<Pathname:..> + # # #<Pathname:ruby.h> + # # #<Pathname:.> + # # => nil + # ``` + # + # With no block given, returns a new Enumerator. + def each_entry(&block) # :yield: pathname + return to_enum(__method__) unless block_given? + Dir.foreach(@path) {|f| yield self.class.new(f) } + end + + # See <tt>Dir.mkdir</tt>. Create the referenced directory. + def mkdir(...) Dir.mkdir(@path, ...) end + + # See <tt>Dir.rmdir</tt>. Remove the referenced directory. + def rmdir() Dir.rmdir(@path) end + + # See <tt>Dir.open</tt>. + def opendir(&block) # :yield: dir + Dir.open(@path, &block) + end +end + +class Pathname # * mixed * + # + # :markup: markdown + # + # call-seq: + # unlink -> 1 or 0 + # + # Removes the file or directory represented by `self`, using: + # + # - File.unlink, if `self` represents a file; returns `1`. + # - Dir.unlink, if `self` represents a directory; returns `0`. + # + # Examples: + # + # ```ruby + # Pathname(Tempfile.create).unlink # => 1 + # Pathname(Pathname.mktmpdir).unlink # => 0 + # ``` + # + def unlink() + Dir.unlink @path + rescue Errno::ENOTDIR + File.unlink @path + end + alias delete unlink +end + +class Pathname + undef =~ if Kernel.method_defined?(:=~) +end + +module Kernel + # Creates a Pathname object. + def Pathname(path) # :doc: + return path if Pathname === path + Pathname.new(path) + end + module_function :Pathname +end |
