Hi all,
I''ve been prototyping a Windows-only Dir class for Ruby. Below is what
I''ve got so far. The class methods were easy, except for Dir.glob. The
code in dir.c is just nasty, and I can''t help but think that it could
be
heavily refactored. I did come across this link:
http://www.codeproject.com/file/fileglob.asp
But I haven''t investigated it yet. Anyone who wants to take a stab at a
pure Ruby Dir.glob is welcome to try. I made one attempt to convert the
pattern to Regexp objects, but it got pretty hairy, especially with
''[a-z]'' type ranges, i.e. match only one character instead of
any one
character. I''d be interested to see what techniques anyone else comes
up
with.
The other issue is how to design Dir.new. I thought the smart thing to
do would be to store the handle returned by FindFirstFile(), but then I
realized that the handle returned by FindFirstFile() isn''t compatible
with the handle used for SetFilePointer(), which would make seek/tell
difficult to implement. On the other hand, if we use CreateFile(), its
handle is not compatible with FindFirstFile() or FindNextFile(), making
read/each difficult to implement.
I make two refactoring decisions for now - Dir.foreach has been skipped.
The Dir.entries now takes an optional block. Dir.open has been skipped.
The Dir.new method now takes an optional block.
Anyway, suggestions welcome on the code below. BTW, you''ll want to grab
the latest windows-pr code from CVS for some of this code to work.
Regards,
Dan
# dir.rb
require ''windows/file''
require ''windows/error''
require ''windows/unicode''
require ''windows/directory''
require ''windows/process''
require ''windows/handle''
# Struct sizes (ANSI/Wide)
#
# WIN32_FIND_DATA: 320/592
# [0,4] => dwFileAttributes
# [4,8] => ftCreationTime
# [12,8] => ftLastAccessTime
# [20,8] => ftLastWriteTime
# [28,4] => nFileSizeHigh
# [32,4] => nFileSizeLow
# [36,4] => dwReserved0
# [40,4] => dwReserved1
# [44,260/520] => cFileName[MAX_PATH]
# [304,14] => cAlternateFileName[14]
class MyDir
include Windows::Error
include Windows::File
include Windows::Unicode
include Windows::Directory
include Windows::Process
include Windows::Handle
extend Windows::Error
extend Windows::File
extend Windows::Unicode
extend Windows::Directory
extend Windows::Process
extend Windows::Handle
MAX_PATH = 260
def self.chdir(dir = nil, &block)
if dir.nil?
buf = 0.chr * 1024 # 32k is the official limit
if GetEnvironmentVariable(''USERPROFILE'', buf,
buf.size) == 0
if GetEnvironmentVariable(''HOME'', buf, buf.size)
== 0
raise ArgumentError, ''USERPROFILE/HOME not
set''
end
end
dir = buf.unpack("Z*")[0]
end
if block_given?
begin
buf = 0.chr * MAX_PATH
if GetCurrentDirectory(buf.length, buf) == 0
raise ArgumentError, get_last_error
else
# MSDN says the drive letter could be dropped,
# and that GetFullPathName should be called just in case.
current = buf.unpack("Z*")[0]
buf2 = 0.chr * MAX_PATH
if GetFullPathName(current, buf2.length, buf2, 0) == 0
raise ArgumentError, get_last_error
end
current = buf2.unpack("Z*")[0]
end
unless SetCurrentDirectory(dir)
raise ArgumentError, get_last_error
end
block.call
ensure
SetCurrentDirectory(current)
end
else
unless SetCurrentDirectory(dir)
raise ArgumentError, get_last_error
end
end
end
def self.delete(dirname)
unless RemoveDirectory(dirname)
raise ArgumentError, get_last_error
end
end
# Blend entries and foreach into one method
def self.entries(dirname)
dirname += "\\*"
fdata = 0.chr * 320 # 580 if wide
array = block_given? ? [] : nil
hfind = FindFirstFile(dirname, fdata)
if hfind == INVALID_HANDLE_VALUE
raise ArgumentError, get_last_error
end
file = fdata[44, MAX_PATH].unpack("Z*")[0]
if block_given?
yield file
else
array << file
end
while FindNextFile(hfind, fdata)
file = fdata[44, MAX_PATH].unpack("Z*")[0]
if block_given?
yield file
else
array << file
end
end
error = GetLastError()
FindClose(hfind)
if error != ERROR_NO_MORE_FILES
raise get_last_error(error)
end
array
end
def self.getwd
buf = 0.chr * MAX_PATH
if GetCurrentDirectory(buf.length, buf) == 0
raise ArgumentError, get_last_error
end
buf.unpack("Z*")[0]
end
# The ''permissions'' could be a Security::Attributes object
# of some sort.
#
def self.mkdir(dirname, permissions = nil)
unless CreateDirectory(dirname, permissions)
raise ArgumentError, get_last_error
end
end
attr_reader :path
# Blend new and open into one method
def initialize(path)
@path = path + "\\*"
@path.tr!(File::SEPARATOR, File::ALT_SEPARATOR)
@fdata = 0.chr * 320 # 580 if wide
@handle = FindFirstFile(@path, @fdata)
if @handle == INVALID_HANDLE_VALUE
raise ArgumentError, get_last_error
end
if block_given?
begin
yield @handle
ensure
close
end
end
@pos = 0
@handle
end
def close
FindClose(@handle)
end
# Broken because the @handle isn''t valid
def pos
SetFilePointer(@handle, 0, 0, FILE_CURRENT)
end
def read
if @pos > 0
@fdata = 0.chr * 320
unless FindNextFile(@handle, @fdata)
raise ArgumentError, get_last_error
end
end
@pos += 1
@fdata[44, MAX_PATH].unpack("Z*")[0]
end
def each
@fdata = 0.chr * 320
while FindNextFile(@handle, @fdata)
yield @fdata[44, MAX_PATH].unpack("Z*")[0]
end
end
def rewind
unless SetFilePointerEx(@handle,0,nil,0)
raise ArgumentError, get_last_error
end
end
# class level aliases
class << self
alias open new
alias foreach entries
alias pwd getwd
alias rmdir delete
alias unlink delete
end
end
if $0 == __FILE__
dir = MyDir.new(Dir.pwd)
p dir.pos
dir.read
p dir.pos
dir.close
end