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