Patches item #1939, was opened at 2005-05-21 12:34 You can respond by visiting: http://rubyforge.org/tracker/?func=detail&atid=1700&aid=1939&group_id=426 Category: Ruby1.8 Group: None Status: Open Resolution: None Priority: 3 Submitted By: Daniel Berger (djberg96) Assigned to: Nobody (None) Summary: Pathname, totally revamped Initial Comment: Hi all, Attached is a complete replacement for the current Pathname class. Here are the primary differences: * It is a subclass of String (and thus, mixes in Enumerable). * It has sensical to_a and root instance methods. * It works on Windows and Unix. The current implementation does not work with Windows path names. * The cleanpath method works differently. * The + method auto cleans. On Win32 systems, the Win32API package is used and a few of Microsoft's builtin path handling functions are used instead of a custom method. In addition, attached is the test suite for Win32. I have a separate one for Unix, but it seems I can only attach one file. You can also check out the code in its entirety from cvs. I'm currently storing this project at http://ruby-miscutils.sf.net. The 'facade' requirement can be removed and that module can be inlined if you don't like the dependency. It's only about 20 lines of code. Here's the new pathname.rb: # == Synopsis # # Pathname represents a path name on a filesystem. A Pathname can be # relative or absolute. It does not matter whether the path exists or not. # # All functionality from File, FileTest, and Dir is included, using a facade # pattern. # # This class works on both Unix and Win32, including UNC path names. Note # that forward slashes are converted to backslashes on Win32 systems. # # == Usage # # require "pathname" # # # Unix # path1 = Pathname.new("/foo/bar/baz") # path2 = Pathname.new("../zap") # # path1 + path2 # "/foo/bar/zap" # path1.dirname # "/foo/bar" # # # Win32 # path1 = Pathname.new("C:\foo\bar\baz") # path2 = Pathname.new("..\zap") # # path1 + path2 # "C:\foo\bar\zap" # path1.exists? # Does the path exist? # # == Author # # Daniel J. Berger # djberg96 at yahoo dot com # imperator on IRC (irc.freenode.net) # # == Copyright # Copyright (c) 2005 Daniel J. Berger. # Licensed under the same terms as Ruby itself. # require "facade" require "Win32API" if File::ALT_SEPARATOR class Pathname < String extend Facade facade File facade Dir if File::ALT_SEPARATOR @@PathStripToRoot = Win32API.new("shlwapi","PathStripToRoot","P","L") @@PathIsUNC = Win32API.new("shlwapi","PathIsUNC","P","L") @@PathCanonicalize = Win32API.new("shlwapi","PathCanonicalize","PP","L") @@PathAppend = Win32API.new("shlwapi","PathAppend","PP","L") @@PathGetDriveNumber = Win32API.new("shlwapi","PathGetDriveNumber","P","L") end VERSION = "1.0.0" MAX_PATH = 255 # Creates and returns a new Pathname object. # # On Win32 systems, all forward slashes are replaced with backslashes. def initialize(path) @sep = File::ALT_SEPARATOR || File::SEPARATOR # Convert forward slashes to backslashes on Win32 path.tr!("/",@sep) if File::ALT_SEPARATOR super(path) end # Splits a pathname into pieces based on the path separator. For example, # "/foo/bar/baz" would return a three element array of ['foo','bar','baz']. def to_a array = split(@sep) # Split string by path separator array.delete("") # Remove empty elements array end # Returns the root directory of the path, or '.' if there is no root # directory. # # On Unix, this means the '/' character. On Win32 systems, this can # refer to the drive letter, or the server and share path if the path # is a UNC path. def root dir = "." if File::ALT_SEPARATOR # We only want the portion up to the first '\0' if @@PathStripToRoot.call(self) > 0 dir = self.split(0.chr).first end else dir = File.dirname(self) while dir != "/" && dir != "." dir = File.dirname(dir) end end dir = "." if dir.empty? dir end # Returns whether or not the path consists only of a root directory. def root? self == root end # Win32 only # # Determines if the string is a valid Universal Naming Convention (UNC) # for a server and share path. def unc? unless File::ALT_SEPARATOR raise NoMethodError, "not supported on this platform" end @@PathIsUNC.call(self) > 0 end # Win32 only # # Returns the drive number that corresponds to the root, or nil if not # applicable. # # For example, Pathname.new("C:\foo").drive_number would return 2. def drive_number unless File::ALT_SEPARATOR raise NoMethodError, "not supported on this platform" end num = @@PathGetDriveNumber.call(self) num >= 0 ? num : nil end # Pathnames may only be compared against other Pathnames, not strings. def <=>(string) return nil unless string.kind_of?(Pathname) super end # Adds two Pathname objects together, or a Pathname and a String. It # also automatically cleans the Pathname. # # Example: # path1 = '/foo/bar' # path2 = '../baz' # path1 + path2 # '/foo/baz' # # Adding a root path to an existing path merely replaces the current # path. Adding '.' to an existing path does nothing. def +(string) unless string.kind_of?(Pathname) string = Pathname.new(string) end # Any path plus "." is the same directory return self if string == "." # Use the builtin PathAppend method if on Windows - much easier if File::ALT_SEPARATOR buf = 0.chr * MAX_PATH buf[0..self.length-1] = self @@PathAppend.call(buf, string << 0.chr) buf.strip! return Pathname.new(buf) # PathAppend cleans automatically end # If the string is an absolute directory, return it return string if string.absolute? array = self.to_a + string.to_a new_string = array.join(@sep) unless self.relative? || File::ALT_SEPARATOR new_string = @sep + new_string # Add root path back if needed end Pathname.new(new_string).clean end # Returns whether or not the path is an absolute path. def absolute? root != "." end # Returns whether or not the path is a relative path. def relative? root == "." end # Removes unnecessary '.' paths and ellides '..' paths appropriately. def clean return self if self.empty? if File::ALT_SEPARATOR path = 0.chr * MAX_PATH if @@PathCanonicalize.call(path, self) > 0 return Pathname.new(path.split(0.chr).first) else return self end end final = [] self.to_a.each{ |element| next if element == "." final.push(element) if element == ".." && self != ".." 2.times{ final.pop } end } final = final.join(@sep) final = root + final if root != "." final = "." if final.empty? Pathname.new(final) end alias :cleanpath :clean #-- IO methods not handled by facade def foreach(*args, &block) IO.foreach(self, *args, &block) end def read(*args) IO.read(self, *args) end def readlines(*args) IO.readlines(self, *args) end def sysopen(*args) IO.sysopen(self, *args) end #-- Dir methods not handled by facade def glob(*args) if block_given? Dir.glob(*args){ |file| yield Pathname.new(file) } else Dir.glob(*args).map{ |file| Pathname.new(file) } end end def chdir(&block) Dir.chdir(self, &block) end def entries Dir.entries(self).map{ |file| Pathname.new(file) } end def mkdir(*args) Dir.mkdir(self, *args) end def opendir(&block) Dir.open(self, &block) end #-- File methods not handled by facade def chmod(mode) File.chmod(mode, self) end def lchmod(mode) File.lchmod(mode, self) end def chown(owner, group) File.chown(owner, group, self) end def lchown(owner, group) File.lchown(owner, group, self) end def fnmatch(pattern, *args) File.fnmatch(pattern, self, *args) end def fnmatch?(pattern, *args) File.fnmatch?(pattern, self, *args) end def link(old) File.link(old, self) end def open(*args, &block) File.open(self, *args, &block) end def rename(name) File.rename(self, name) end def symlink(old) File.symlink(old, self) end def truncate(length) File.truncate(self, length) end def utime(atime, mtime) File.utime(atime, mtime, self) end def basename(*args) File.basename(self, *args) end def expand_path(*args) File.expand_path(self, *args) end end Regards, Dan ---------------------------------------------------------------------- You can respond by visiting: http://rubyforge.org/tracker/?func=detail&atid=1700&aid=1939&group_id=426