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 |