# 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.new('/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) # => # # # 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 # => # # File.dirname(path) # => "lib" # # pn.basename # => # # File.basename(path) # => "fileutils.rb" # # pn.split # => [#, #] # 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: # # # # # # # 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') # => # # p1.absolute? # => true # p2 = p1 + 'ruby/4.0' # => # # p3 = p1.parent # => # # p4 = p2.relative_path_from(p3) # => # # p4.absolute? # => false # p5 = Pathname.new('.') # => # # p6 = p5 + 'usr/../var' # => # # p6.cleanpath # => # # p6.realpath # => # # p6.children.take(2) # # => [#, #] # # == 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: if File::FNM_SYSCASE.nonzero? # Avoid #zero? here because #casecmp can return nil. private def same_paths?(a, b) a.casecmp(b) == 0 end else private def same_paths?(a, b) a == b end end attr_reader :path protected :path # :startdoc: # call-seq: # Pathname.new(path) -> new_pathname # # Returns a new \Pathname object based on the given +path+, # via File.path(path).dup. # the +path+ may be a String, a File, a Dir, or another \Pathname; # see File.path: # # Pathname.new('.') # => # # Pathname.new('/usr/bin') # => # # Pathname.new(File.new('LEGAL')) # => # # Pathname.new(Dir.new('.')) # => # # Pathname.new(Pathname.new('.')) # => # # 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.new('lib') # pn == Pathname.new('lib') # => true # pn == Pathname.new('./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 if File::ALT_SEPARATOR # Separator list string. separator_list = Regexp.quote "#{File::ALT_SEPARATOR}#{File::SEPARATOR}" # Regexp that matches a separator. SEPARATOR_PAT = /[#{separator_list}]/ else separator_list = Regexp.quote File::SEPARATOR SEPARATOR_PAT = /#{separator_list}/ end SEPARATOR_PAT.freeze private_constant :SEPARATOR_PAT # 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) || File.dirname(path) == path stack.push path path = File.dirname(path) 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 # split_names(path) -> prefix, [name, ...] def split_names(path) # :nodoc: names = [] while r = chop_basename(path) path, basename = r names.unshift basename end return path, names end private :split_names def prepend_prefix(prefix, relpath) # :nodoc: if relpath.empty? File.dirname(prefix) elsif has_separator?(prefix) prefix = File.dirname(prefix) prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a' prefix + relpath else prefix + relpath end end private :prepend_prefix # 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 pathname with a single-dot entry: # # Pathname.new('').cleanpath # => # # # Separators # # A lone separator is preserved: # # Pathname.new('/').cleanpath # => # # # Non-lone trailing separators are removed: # # Pathname.new('foo/////').cleanpath # => # # Pathname.new('foo/').cleanpath # => # # # Multiple embedded separators are reduced to a single separator: # # Pathname.new('foo///bar').cleanpath # => # # # Multiple leading separators are reduced: # # # On Windows, where File.dirname('//') == '//'. # Pathname.new('/////foo').cleanpath # => # # Pathname.new('/////').cleanpath # => # # # Otherwise, where File.dirname('//') == '/'. # Pathname.new('/////foo').cleanpath # => # # Pathname.new('/////').cleanpath # => # # # Single-Dot Entries # # A lone single-dot entry is preserved: # # Pathname.new('.').cleanpath # => # # # A non-lone single-dot entry, regardless of its location, is removed: # # Pathname.new('foo/././././bar').cleanpath # => # # Pathname.new('./foo/./././bar').cleanpath # => # # Pathname.new('foo/./././bar/./').cleanpath # => # # # Double-Dot Entries # # A lone double-dot entry is preserved: # # Pathname.new('..').cleanpath # => # # # When a non-lone double-dot entry is preceded by a named entry, both are removed: # # Pathname.new('foo/..').cleanpath # => # # Pathname.new('foo/../bar').cleanpath # => # # Pathname.new('foo/../bar/..').cleanpath # => # # Pathname.new('foo/bar/./../..').cleanpath # => # # # When a non-lone double-dot entry is _not_ preceded by a named entry, # it is preserved: # # Pathname.new('../..').cleanpath # => # # # A non-lone meaningless double-dot entry is removed: # # Pathname.new('/..').cleanpath # => # # Pathname.new('/../..').cleanpath # => # # # Symbolic Links # # If the path may contain {symbolic links}[https://en.wikipedia.org/wiki/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.new('a/').cleanpath # => # # Pathname.new('a/').cleanpath(true) # => # # # Pathname.new('a/.').cleanpath # => # # Pathname.new('a/.').cleanpath(true) # => # # # Pathname.new('a/./').cleanpath # => # # Pathname.new('a/./').cleanpath(true) # => # # # Pathname.new('a/b/.').cleanpath # => # # Pathname.new('a/b/.').cleanpath(true) # => # # # Pathname.new('a/../.').cleanpath # => # # Pathname.new('a/../.').cleanpath(true) # => # # # Pathname.new('a/b/../../../../c/../d').cleanpath # # => # # Pathname.new('a/b/../../../../c/../d').cleanpath(true) # # => # # 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 # has_trailing_separator?(path) -> bool def has_trailing_separator?(path) # :nodoc: if r = chop_basename(path) pre, basename = r pre.length + basename.length < path.length else false end end private :has_trailing_separator? # add_trailing_separator(path) -> path def add_trailing_separator(path) # :nodoc: if File.basename(path + 'a') == 'a' path else File.join(path, "") # xxx: Is File.join is appropriate to add separator? end end private :add_trailing_separator def del_trailing_separator(path) # :nodoc: if r = chop_basename(path) pre, basename = r pre + basename elsif /#{SEPARATOR_PAT}+\z/o =~ path $` + File.dirname(path)[/#{SEPARATOR_PAT}*\z/o] else path end end private :del_trailing_separator 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 self + '..'. 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 # # Iterates over each component of the path. # # Pathname.new("/usr/bin/ruby").each_filename {|filename| ... } # # yields "usr", "bin", and "ruby". # # Returns an Enumerator if no block was given. # # enum = Pathname.new("/usr/bin/ruby").each_filename # # ... do stuff ... # enum.each { |e| ... } # # yields "usr", "bin", and "ruby". # def each_filename # :yield: filename return to_enum(__method__) unless block_given? _, names = split_names(@path) names.each {|filename| yield filename } nil end # Iterates over and yields a new Pathname object # for each element in the given path in descending order. # # Pathname.new('/path/to/some/file.rb').descend {|v| p v} # # # # # # # # # # # # Pathname.new('path/to/some/file.rb').descend {|v| p v} # # # # # # # # # # Returns an Enumerator if no block was given. # # enum = Pathname.new("/usr/bin/ruby").descend # # ... do stuff ... # enum.each { |e| ... } # # yields Pathnames /, /usr, /usr/bin, and /usr/bin/ruby. # # It doesn't access the filesystem. # 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.new('/path/to/some/file.rb').ascend {|dirname| p dirname} # # # # # # # # # # # # 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.new('foo') # => # # pn + 'bar' # => # # pn + File.new('LEGAL') # => # # pn + Dir.new('lib') # => # # pn + Pathname.new('bar') # => # # # When +other+ specifies a relative path (see #relative?), # it is combined with +self+ to form a new pathname: # # Pathname.new('/a/b') + 'c' # => # # # Extra component separators ('/') are removed: # # Pathname.new('/a/b/') + 'c' # => # # # Extra current-directory components ('.') are removed: # # Pathname.new('a') + '.' # => # # Pathname.new('.') + 'a' # => # # Pathname.new('.') + '.' # => # # # Parent-directory components ('..') are: # # - Resolved, when possible: # # Pathname.new('a') + '..' # => # # Pathname.new('a/b') + '..' # => # # Pathname.new('/') + '../a' # => # # Pathname.new('a') + '../b' # => # # Pathname.new('a/b') + '../c' # => # # Pathname.new('a//b/c') + '../d//e' # => # # # - Removed, when not needed: # # Pathname.new('/') + '..' # => # # # - Retained, when needed: # # Pathname.new('..') + '..' # => # # Pathname.new('..') + '../a' # => # # # When +other+ specifies an absolute path (see #absolute?), # equivalent to Pathname.new(other.to_s): # # Pathname.new('/a') + '/b/c' # => # # # Occurrences of '/', '.', and '..' are preserved: # # Pathname.new('/a') + '//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.new('/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 # # Returns the children of the directory (files and subdirectories, not # recursive) as an array of Pathname objects. # # By default, the returned pathnames will have enough information to access # the files. If you set +with_directory+ to +false+, then the returned # pathnames will contain the filename only. # # For example: # pn = Pathname("/usr/lib/ruby/1.8") # pn.children # # -> [ Pathname:/usr/lib/ruby/1.8/English.rb, # Pathname:/usr/lib/ruby/1.8/Env.rb, # Pathname:/usr/lib/ruby/1.8/abbrev.rb, ... ] # pn.children(false) # # -> [ Pathname:English.rb, Pathname:Env.rb, Pathname:abbrev.rb, ... ] # # Note that the results never contain the entries +.+ and +..+ in # the directory because they are not children. # def children(with_directory=true) with_directory = false if @path == '.' result = [] Dir.foreach(@path) {|e| next if e == '.' || e == '..' if with_directory result << self.class.new(File.join(@path, e)) else result << self.class.new(e) end } result end # Iterates over the children of the directory # (files and subdirectories, not recursive). # # It yields Pathname object for each child. # # By default, the yielded pathnames will have enough information to access # the files. # # If you set +with_directory+ to +false+, then the returned pathnames will # contain the filename only. # # Pathname("/usr/local").each_child {|f| p f } # #=> # # # # # # # # # # # # # # # # # # # # # # # # Pathname("/usr/local").each_child(false) {|f| p f } # #=> # # # # # # # # # # # # # # # # # # # # # # # # Note that the results never contain the entries +.+ and +..+ in # the directory because they are not children. # # See Pathname#children # 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 * # # #each_line iterates over the line in the file. It yields a String object # for each line. # # This method has existed since 1.8.1. # 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 self.to_s. # # 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 File.readlines. Returns all the lines from the file. def readlines(...) File.readlines(@path, ...) end # See File.sysopen. 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 "\u0000": # # 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+: # # filepath = 't.tmp' # pn = Pathname.new(filepath) # File.exist?(filepath) # => false # pn.atime # Raises Errno::ENOENT: No such file or directory # File.write(filepath, 'foo') # pn.atime # => 2026-03-22 13:49:44.5165608 -0500 # File.read(filepath) # pn.atime # => 2026-03-22 13:49:57.5359349 -0500 # File.delete(filepath) # # dirpath = 'tmp' # Dir.mkdir(dirpath) # pn = Pathname.new(dirpath) # pn.atime # => 2026-03-31 11:46:35.4813492 -0500 # Dir.empty?(dirname) # => true # pn.atime # => 2026-03-31 11:51:10.1210092 -0500 # Dir.delete(dirpath) # # See {File System Timestamps}[rdoc-ref:file/timestamps.md]. def atime() File.atime(@path) end # call-seq: # birthtime -> new_time # # Returns a new Time object containing the create time of the entry # represented by +self+: # # filepath = 't.tmp' # pn = Pathname.new(filepath) # pn.birthtime # Raises Errno::ENOENT: No such file or directory # file = File.open(filepath, 'w') # pn.birthtime # => 2026-04-14 16:14:47.494846 -0500 # file.birthtime # => 2026-04-14 16:14:47.494846 -0500 # file.write('foo') # pn.birthtime # => 2026-04-14 16:14:47.494846 -0500 # file.close # pn.birthtime # => 2026-04-14 16:14:47.494846 -0500 # File.delete(filepath) # pn.birthtime # Raises Errno::ENOENT: No such file or directory # # See {File System Timestamps}[rdoc-ref:file/timestamps.md]. def birthtime() File.birthtime(@path) end # See File.ctime. Returns last (directory entry, not file) change time. def ctime() File.ctime(@path) end # See File.mtime. Returns last modification time. def mtime() File.mtime(@path) end # See File.chmod. Changes permissions. def chmod(mode) File.chmod(mode, @path) end # See File.lchmod. def lchmod(mode) File.lchmod(mode, @path) end # See File.chown. Change owner and group of file. def chown(owner, group) File.chown(owner, group, @path) end # See File.lchown. def lchown(owner, group) File.lchown(owner, group, @path) end # See File.fnmatch. Return +true+ if the receiver matches the given # pattern. def fnmatch(pattern, ...) File.fnmatch(pattern, @path, ...) end # See File.fnmatch? (same as #fnmatch). def fnmatch?(pattern, ...) File.fnmatch?(pattern, @path, ...) end # See File.ftype. Returns "type" of file ("file", "directory", # etc). def ftype() File.ftype(@path) end # See File.link. Creates a hard link. def make_link(old) File.link(old, @path) end # See File.open. Opens the file for reading or writing. def open(...) # :yield: file File.open(@path, ...) end # See File.readlink. Read symbolic link. def readlink() self.class.new(File.readlink(@path)) end # See File.rename. Rename the file. def rename(to) File.rename(@path, to) end # See File.stat. Returns a File::Stat object. def stat() File.stat(@path) end # See File.lstat. def lstat() File.lstat(@path) end # See File.symlink. Creates a symbolic link. def make_symlink(old) File.symlink(old, @path) end # See File.truncate. Truncate the file to +length+ bytes. def truncate(length) File.truncate(@path, length) end # See File.utime. 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 '', returns all of the last entry: # # Pathname.new('foo/bar/baz/bat.txt').basename # => # # Pathname.new('foo/bar/baz').basename # => # # # File::SEPARATOR # => "/" # Pathname.new('foo/bar.txt////').basename # => # # File::ALT_SEPARATOR # => "\\" # On Windows. # Pathname.new('foo/bar.txt//\\\\//').basename # => # # # When +suffix+ is '.*', # the last {filename extension}[https://en.wikipedia.org/wiki/Filename_extension], # if any, is removed: # # Pathname.new('foo/bar.txt').basename('.*') # => # # Pathname.new('foo/bar.txt.old').basename('.*') # => # # Pathname.new('foo/bar').basename('.*') # => # # # When +suffix+ is any string other than '' or '.*', # the matching trailing substring, if any, is removed: # # Pathname.new('foo/bar.txt').basename('.txt') # => # # Pathname.new('foo/bar.txt').basename('txt') # => # # Pathname.new('foo/bar.txt').basename('*') # => # # Pathname.new('foo/bar.txt').basename('.') # => # # def basename(...) self.class.new(File.basename(@path, ...)) end # See File.dirname. Returns all but the last component of the path. def dirname() self.class.new(File.dirname(@path)) end # See File.extname. Returns the file's extension. def extname() File.extname(@path) end # See File.expand_path. def expand_path(...) self.class.new(File.expand_path(@path, ...)) end # See File.split. 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 * # call-seq: # blockdev? => true or false # # Returns whether +self+ represents a block device # (i.e., a random-access device): # # Pathname.new('/dev/nvme0n1').blockdev? # => true # Pathname.new('/dev/loop0').blockdev? # => true # Pathname.new('/dev/tty').blockdev? # => false # Pathname.new('/dev/null').blockdev? # => false # Pathname.new('nosuch').blockdev? # => false # # The returned value is OS-dependent; on Windows, almost always +false+. def blockdev?() FileTest.blockdev?(@path) end # call-seq: # chardev? => true or false # # Returns whether +self+ represents a character device # (i.e., a sequential-access device): # # Pathname.new('/dev/tty').chardev? # => true # Pathname.new('/dev/null').chardev? # => true # Pathname.new('/dev/nvme0n1').chardev? # => false # Pathname.new('/dev/loop0').chardev? # => false # Pathname.new('nosuch').chardev? # => false # # The returned value is OS-dependent; on Windows, almost always +false+. def chardev?() FileTest.chardev?(@path) end # Tests the file is empty. # # See Dir#empty? and FileTest.empty?. def empty? if FileTest.directory?(@path) Dir.empty?(@path) else File.empty?(@path) end end # See FileTest.executable?. def executable?() FileTest.executable?(@path) end # See FileTest.executable_real?. def executable_real?() FileTest.executable_real?(@path) end # See FileTest.exist?. def exist?() FileTest.exist?(@path) end # See FileTest.grpowned?. def grpowned?() FileTest.grpowned?(@path) end # See FileTest.directory?. def directory?() FileTest.directory?(@path) end # See FileTest.file?. def file?() FileTest.file?(@path) end # See FileTest.pipe?. def pipe?() FileTest.pipe?(@path) end # See FileTest.socket?. def socket?() FileTest.socket?(@path) end # See FileTest.owned?. def owned?() FileTest.owned?(@path) end # See FileTest.readable?. def readable?() FileTest.readable?(@path) end # See FileTest.world_readable?. def world_readable?() File.world_readable?(@path) end # See FileTest.readable_real?. def readable_real?() FileTest.readable_real?(@path) end # See FileTest.setuid?. def setuid?() FileTest.setuid?(@path) end # See FileTest.setgid?. def setgid?() FileTest.setgid?(@path) end # See FileTest.size. def size() FileTest.size(@path) end # See FileTest.size?. def size?() FileTest.size?(@path) end # See FileTest.sticky?. def sticky?() FileTest.sticky?(@path) end # See FileTest.symlink?. def symlink?() FileTest.symlink?(@path) end # See FileTest.writable?. def writable?() FileTest.writable?(@path) end # See FileTest.world_writable?. def world_writable?() File.world_writable?(@path) end # See FileTest.writable_real?. def writable_real?() FileTest.writable_real?(@path) end # See FileTest.zero?. def zero?() FileTest.zero?(@path) end end class Pathname # * Dir * # call-seq: # glob(patterns, **kwargs) → array_of_pathnames # glob(patterns, **kwargs) {|pathname| ... } → nil # # Calls Dir.glob(patterns, **kwargs), 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 **kwargs are passed through to Dir.glob; # see the documentation there. # # With no block given, returns an array of \Pathname objects; # each is Pathname.new(entry_name) for an entry name returned by Dir.glob. # # Pathname.glob('*').take(3) # # => [#, #, #] # Pathname.glob(['o*', 'a*']).take(3) # # => [#, #, #] # # With a block given, calls the block with each pathname # Pathname.new(entry_name), # 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) # # => [#, #, #] # # 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.glob('*', base: 'lib/bundler').take(2) # # => [#, #] # # 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") # #=> [#, #] # # 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 Pathname.new(Dir.getwd)): # # Pathname.getwd # => # # def Pathname.getwd() self.new(Dir.getwd) end class << self alias pwd getwd end # Return the entries (files and subdirectories) in the directory, each as a # Pathname object. def entries() Dir.entries(@path).map {|f| self.class.new(f) } end # Iterates over the entries (files and subdirectories) in the directory. It # yields a Pathname object for each entry. # # This method has existed since 1.8.1. def each_entry(&block) # :yield: pathname return to_enum(__method__) unless block_given? Dir.foreach(@path) {|f| yield self.class.new(f) } end # See Dir.mkdir. Create the referenced directory. def mkdir(...) Dir.mkdir(@path, ...) end # See Dir.rmdir. Remove the referenced directory. def rmdir() Dir.rmdir(@path) end # See Dir.open. def opendir(&block) # :yield: dir Dir.open(@path, &block) end end class Pathname # * mixed * # Removes a file or directory, using File.unlink or # Dir.unlink as necessary. 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