core/dir_glob.rb
class Dir
module Glob
class Node
def initialize(nxt, flags)
@flags = flags
@next = nxt
@separator = nil
end
private :initialize
attr_writer :separator
def separator
@separator || "/"
end
def path_join(parent, ent)
return ent unless parent
if parent == "/"
"/#{ent}"
else
"#{parent}#{separator}#{ent}"
end
end
end
class ConstantDirectory < Node
def initialize(nxt, flags, dir)
super nxt, flags
@dir = dir
end
private :initialize
def call(env, path)
full = path_join(path, @dir)
# Don't check if full exists. It just costs us time
# and the downstream node will be able to check properly.
@next.call env, full
end
end
class ConstantEntry < Node
def initialize(nxt, flags, name)
super nxt, flags
@name = name
end
private :initialize
def call(env, parent)
path = path_join(parent, @name)
if File.exist? path
env.matches << path
end
end
end
class RootDirectory < Node
def call(env, path)
@next.call env, "/"
end
end
class RecursiveDirectories < Node
def call(env, start)
return if !start || !File.exist?(start)
# Even though the recursive entry is zero width
# in this case, its left separator is still the
# dominant one, so we fix things up to use it.
switched = @next.dup
switched.separator = @separator
switched.call env, start
stack = [start]
allow_dots = ((@flags & File::FNM_DOTMATCH) != 0)
until stack.empty?
path = stack.pop
begin
dir = Dir.new(path)
rescue Errno::ENOTDIR
next
end
while ent = dir.read
next if ent == "." || ent == ".."
full = path_join(path, ent)
stat = File::Stat.lstat full
if stat.directory? and (allow_dots or ent.getbyte(0) != 46) # ?.
stack << full
@next.call env, full
end
end
dir.close
end
end
end
class StartRecursiveDirectories < Node
def call(env, start)
raise "invalid usage" if start
# Even though the recursive entry is zero width
# in this case, its left separator is still the
# dominant one, so we fix things up to use it.
if @separator
switched = @next.dup
switched.separator = @separator
switched.call env, start
else
@next.call env, start
end
stack = []
allow_dots = ((@flags & File::FNM_DOTMATCH) != 0)
dir = Dir.new(".")
while ent = dir.read
next if ent == "." || ent == ".."
stat = File::Stat.lstat ent
if stat.directory? and (allow_dots or ent.getbyte(0) != 46) # ?.
stack << ent
@next.call env, ent
end
end
dir.close
until stack.empty?
path = stack.pop
dir = Dir.new(path)
while ent = dir.read
next if ent == "." || ent == ".."
full = path_join(path, ent)
stat = File::Stat.lstat full
if stat.directory? and ent.getbyte(0) != 46 # ?.
stack << full
@next.call env, full
end
end
dir.close
end
end
end
class Match < Node
def initialize(nxt, flags, glob)
super nxt, flags
@glob = glob || ""
end
private :initialize
def match?(str)
File.fnmatch @glob, str, @flags
end
end
class DirectoryMatch < Match
def initialize(nxt, flags, glob)
super
@glob.gsub! "**", "*"
end
private :initialize
def call(env, path)
return if path and !File.exist?("#{path}/.")
dir = Dir.new(path ? path : ".")
while ent = dir.read
if match? ent
full = path_join(path, ent)
if File.directory? full
@next.call env, full
end
end
end
dir.close
end
end
class EntryMatch < Match
def call(env, path)
return if path and !File.exist?("#{path}/.")
begin
dir = Dir.new(path ? path : ".")
rescue SystemCallError
return
end
while ent = dir.read
if match? ent
env.matches << path_join(path, ent)
end
end
dir.close
end
end
class DirectoriesOnly < Node
def call(env, path)
if path and File.exist?("#{path}/.")
env.matches << "#{path}/"
end
end
end
class Environment
attr_reader :matches
def initialize(matches=[])
@matches = matches
end
private :initialize
end
def self.path_split(str)
start = 0
ret = []
last_match = nil
while match = %r!/+!.match_from(str, start)
cur_start, cur_end = match.full
ret << str.byteslice(start, cur_start - start)
ret << str.byteslice(cur_start, cur_end - cur_start)
start = cur_end
last_match = match
end
if last_match
ret << last_match.post_match
else
ret << str
end
# Trim from end
if !ret.empty?
while s = ret.last and s.empty?
ret.pop
end
end
ret
end
def self.single_compile(glob, flags=0)
parts = path_split(glob)
if glob.getbyte(-1) == 47 # ?/
last = DirectoriesOnly.new nil, flags
else
file = parts.pop
if /^[a-zA-Z0-9._]+$/.match(file)
last = ConstantEntry.new nil, flags, file
else
last = EntryMatch.new nil, flags, file
end
end
until parts.empty?
last.separator = parts.pop
dir = parts.pop
if dir == "**"
if parts.empty?
last = StartRecursiveDirectories.new last, flags
else
last = RecursiveDirectories.new last, flags
end
elsif /^[^\*\?\]]+$/.match(dir)
while /^[^\*\?\]]+$/.match(parts[-2])
next_sep = parts.pop
next_sect = parts.pop
dir = next_sect << next_sep << dir
end
last = ConstantDirectory.new last, flags, dir
elsif !dir.empty?
last = DirectoryMatch.new last, flags, dir
end
end
if glob.getbyte(0) == 47 # ?/
last = RootDirectory.new last, flags
end
last
end
def self.run(node, matches=[])
env = Environment.new(matches)
node.call env, nil
env.matches
end
def self.glob_cache
@glob_cache
end
def self.glob(pattern, flags, matches=[])
# Rubygems uses Dir[] as a glorified File.exist? to check for multiple
# extensions. So we went ahead and sped up that specific case.
if flags == 0 and
m = /^([a-zA-Z0-9_.\/\s]*[a-zA-Z0-9_.])(?:\{([^{}\/\*\?]*)\})?$/.match(pattern)
# no meta characters, so this is a glorified
# File.exist? check. We allow for a brace expansion
# only as a suffix.
if braces = m[2]
stem = m[1]
braces.split(",").each do |s|
path = "#{stem}#{s}"
if File.exist? path
matches << path
end
end
# Split strips an empty closing part, so we need to add it back in
if braces.getbyte(-1) == 44 # ?,
matches << stem if File.exist? stem
end
else
matches << pattern if File.exist?(pattern)
end
return matches
end
ec_key = nil
if gc = @glob_cache
ec_key = [pattern, flags]
if patterns = gc.retrieve(ec_key)
patterns.each do |node|
run node, matches
end
return matches
end
end
if pattern.include? "{"
patterns = compile(pattern, flags)
gc.set ec_key, patterns if ec_key
patterns.each do |node|
run node, matches
end
elsif node = single_compile(pattern, flags)
gc.set ec_key, [node] if ec_key
run node, matches
else
matches
end
end
def self.compile(pattern, flags=0, patterns=[])
escape = (flags & File::FNM_NOESCAPE) == 0
rbrace = nil
lbrace = nil
# Do a quick search for a { to start the search better
i = pattern.index("{")
# If there was a { found, then search
if i
nest = 0
while i < pattern.size
char = pattern[i]
if char == "{"
lbrace = i if nest == 0
nest += 1
end
if char == "}"
nest -= 1
end
if nest == 0
rbrace = i
break
end
if char == "\\" and escape
i += 1
end
i += 1
end
end
# There was a full {} expression detected, expand each part of it
# recursively.
if lbrace and rbrace
pos = lbrace
front = pattern[0...lbrace]
back = pattern[(rbrace + 1)..-1]
while pos < rbrace
nest = 0
pos += 1
last = pos
while pos < rbrace and not (pattern[pos] == "," and nest == 0)
nest += 1 if pattern[pos] == "{"
nest -= 1 if pattern[pos] == "}"
if pattern[pos] == "\\" and escape
pos += 1
break if pos == rbrace
end
pos += 1
end
brace_pattern = "#{front}#{pattern[last...pos]}#{back}"
compile brace_pattern, flags, patterns
end
# No braces found, match the pattern normally
else
# Don't use .glob here because this code can detect properly
# if a { is a brace or a just a normal character, but .glob can't.
# if .glob is used and there is a { as a normal character, it will
# recurse forever.
if node = single_compile(pattern, flags)
patterns << node
end
end
patterns
end
end
end