mirror of
https://github.com/mistergibson/gxg-web-client.git
synced 2026-08-15 11:34:29 -07:00
460 lines
10 KiB
Ruby
460 lines
10 KiB
Ruby
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
|