class Puppet::Parser::Parser
require 'puppet/parser/functions'
ASTSet = Struct.new(:classes, :definitions, :nodes)
# Define an accessor method for each table. We hide the existence of
# the struct.
[:classes, :definitions, :nodes].each do |name|
define_method(name) do
@astset.send(name)
end
end
AST = Puppet::Parser::AST
attr_reader :file, :interp
attr_accessor :files
# Add context to a message; useful for error messages and such.
def addcontext(message, obj = nil)
obj ||= @lexer
message += " on line %s" % obj.line
if file = obj.file
message += " in file %s" % file
end
return message
end
# Create an AST array out of all of the args
def aryfy(*args)
if args[0].instance_of?(AST::ASTArray)
result = args.shift
args.each { |arg|
result.push arg
}
else
result = ast AST::ASTArray, :children => args
end
return result
end
# Create an AST object, and automatically add the file and line information if
# available.
def ast(klass, hash = nil)
hash ||= {}
unless hash.include?(:line)
hash[:line] = @lexer.line
end
unless hash.include?(:file)
if file = @lexer.file
hash[:file] = file
end
end
return klass.new(hash)
end
# The fully qualifed name, with the full namespace.
def classname(name)
[@lexer.namespace, name].join("::").sub(/^::/, '')
end
def clear
initvars
end
# Raise a Parse error.
def error(message)
if brace = @lexer.expected
message += "; expected '%s'"
end
except = Puppet::ParseError.new(message)
except.line = @lexer.line
if @lexer.file
except.file = @lexer.file
end
raise except
end
def file=(file)
unless FileTest.exists?(file)
unless file =~ /\.pp$/
file = file + ".pp"
end
unless FileTest.exists?(file)
raise Puppet::Error, "Could not find file %s" % file
end
end
if @files.detect { |f| f.file == file }
raise Puppet::AlreadyImportedError.new("Import loop detected")
else
@files << Puppet::Util::LoadedFile.new(file)
@lexer.file = file
end
end
# Find a class definition, relative to the current namespace.
def findclass(namespace, name)
fqfind namespace, name, classes
end
# Find a component definition, relative to the current namespace.
def finddefine(namespace, name)
fqfind namespace, name, definitions
end
# This is only used when nodes are looking up the code for their
# parent nodes.
def findnode(name)
fqfind "", name, nodes
end
# The recursive method used to actually look these objects up.
def fqfind(namespace, name, table)
namespace = namespace.downcase
name = name.downcase
if name =~ /^::/ or namespace == ""
classname = name.sub(/^::/, '')
unless table[classname]
self.load(classname)
end
return table[classname]
end
ary = namespace.split("::")
while ary.length > 0
newname = (ary + [name]).join("::").sub(/^::/, '')
if obj = table[newname] or (self.load(newname) and obj = table[newname])
return obj
end
# Delete the second to last object, which reduces our namespace by one.
ary.pop
end
# If we've gotten to this point without finding it, see if the name
# exists at the top namespace
if obj = table[name] or (self.load(name) and obj = table[name])
return obj
end
return nil
end
# Import our files.
def import(file)
if Puppet[:ignoreimport]
return AST::ASTArray.new(:children => [])
end
# use a path relative to the file doing the importing
if @lexer.file
dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '')
else
dir = "."
end
if dir == ""
dir = "."
end
result = ast AST::ASTArray
# We can't interpolate at this point since we don't have any
# scopes set up. Warn the user if they use a variable reference
pat = file
if pat.index("$")
Puppet.warning(
"The import of #{pat} contains a variable reference;" +
" variables are not interpolated for imports " +
"in file #{@lexer.file} at line #{@lexer.line}"
)
end
files = Puppet::Module::find_manifests(pat, dir)
if files.size == 0
raise Puppet::ImportError.new("No file(s) found for import " +
"of '#{pat}'")
end
files.collect { |file|
parser = Puppet::Parser::Parser.new(@astset)
parser.files = self.files
Puppet.debug("importing '%s'" % file)
unless file =~ /^#{File::SEPARATOR}/
file = File.join(dir, file)
end
begin
parser.file = file
rescue Puppet::AlreadyImportedError
# This file has already been imported to just move on
next
end
# This will normally add code to the 'main' class.
parser.parse
}
end
def initialize(astset = nil)
initvars()
if astset
@astset = astset
end
end
# Initialize or reset all of our variables.
def initvars
@lexer = Puppet::Parser::Lexer.new()
@files = []
@loaded = []
# This is where we store our classes and definitions and nodes.
# Clear each hash, just to help the GC a bit.
if defined?(@astset)
[:classes, :definitions, :nodes].each do |name|
@astset.send(name).clear
end
end
@astset = ASTSet.new({}, {}, {})
end
# Try to load a class, since we could not find it.
def load(classname)
return false if classname == ""
filename = classname.gsub("::", File::SEPARATOR)
loaded = false
# First try to load the top-level module
mod = filename.scan(/^[\w-]+/).shift
unless @loaded.include?(mod)
@loaded << mod
begin
import(mod)
Puppet.info "Autoloaded module %s" % mod
loaded = true
rescue Puppet::ImportError => detail
# We couldn't load the module
end
end
unless filename == mod and ! @loaded.include?(mod)
@loaded << mod
# Then the individual file
begin
import(filename)
Puppet.info "Autoloaded file %s from module %s" % [filename, mod]
loaded = true
rescue Puppet::ImportError => detail
# We couldn't load the file
end
end
return loaded
end
# Split an fq name into a namespace and name
def namesplit(fullname)
ary = fullname.split("::")
n = ary.pop || ""
ns = ary.join("::")
return ns, n
end
# Create a new class, or merge with an existing class.
def newclass(name, options = {})
name = name.downcase
if definitions.include?(name)
raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name
end
code = options[:code]
parent = options[:parent]
# If the class is already defined, then add code to it.
if other = @astset.classes[name]
# Make sure the parents match
if parent and other.parentclass and (parent != other.parentclass)
error("Class %s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line])
end
# This might be dangerous...
if parent and ! other.parentclass
other.parentclass = parent
end
# This might just be an empty, stub class.
if code
tmp = name
if tmp == ""
tmp = "main"
end
Puppet.debug addcontext("Adding code to %s" % tmp)
# Else, add our code to it.
if other.code and code
other.code.children += code.children
else
other.code ||= code
end
end
else
# Define it anew.
# Note we're doing something somewhat weird here -- we're setting
# the class's namespace to its fully qualified name. This means
# anything inside that class starts looking in that namespace first.
args = {:namespace => name, :classname => name, :parser => self}
args[:code] = code if code
args[:parentclass] = parent if parent
@astset.classes[name] = ast AST::HostClass, args
end
return @astset.classes[name]
end
# Create a new definition.
def newdefine(name, options = {})
name = name.downcase
if @astset.classes.include?(name)
raise Puppet::ParseError, "Cannot redefine class %s as a definition" %
name
end
# Make sure our definition doesn't already exist
if other = @astset.definitions[name]
error("%s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line])
end
ns, whatever = namesplit(name)
args = {
:namespace => ns,
:arguments => options[:arguments],
:code => options[:code],
:parser => self,
:classname => name
}
[:code, :arguments].each do |param|
args[param] = options[param] if options[param]
end
@astset.definitions[name] = ast AST::Component, args
end
# Create a new node. Nodes are special, because they're stored in a global
# table, not according to namespaces.
def newnode(names, options = {})
names = [names] unless names.instance_of?(Array)
names.collect do |name|
name = name.to_s.downcase
if other = @astset.nodes[name]
error("Node %s is already defined at %s:%s; cannot redefine" % [other.name, other.file, other.line])
end
name = name.to_s if name.is_a?(Symbol)
args = {
:name => name,
:parser => self
}
if options[:code]
args[:code] = options[:code]
end
if options[:parent]
args[:parentclass] = options[:parent]
end
@astset.nodes[name] = ast(AST::Node, args)
@astset.nodes[name].classname = name
@astset.nodes[name]
end
end
def on_error(token,value,stack)
#on '%s' at '%s' in\n'%s'" % [token,value,stack]
#error = "line %s: parse error after '%s'" %
# [@lexer.line,@lexer.last]
error = "Syntax error at '%s'" % [value]
if brace = @lexer.expected
error += "; expected '%s'" % brace
end
except = Puppet::ParseError.new(error)
except.line = @lexer.line
if @lexer.file
except.file = @lexer.file
end
raise except
end
# how should I do error handling here?
def parse(string = nil)
if string
self.string = string
end
begin
main = yyparse(@lexer,:scan)
rescue Racc::ParseError => except
error = Puppet::ParseError.new(except)
error.line = @lexer.line
error.file = @lexer.file
error.set_backtrace except.backtrace
raise error
rescue Puppet::ParseError => except
except.line ||= @lexer.line
except.file ||= @lexer.file
raise except
rescue Puppet::Error => except
# and this is a framework error
except.line ||= @lexer.line
except.file ||= @lexer.file
raise except
rescue Puppet::DevError => except
except.line ||= @lexer.line
except.file ||= @lexer.file
raise except
rescue => except
error = Puppet::DevError.new(except.message)
error.line = @lexer.line
error.file = @lexer.file
error.set_backtrace except.backtrace
raise error
end
if main
# Store the results as the top-level class.
newclass("", :code => main)
end
return @astset
ensure
@lexer.clear
end
# See if any of the files have changed.
def reparse?
if file = @files.detect { |file| file.changed? }
return file.stamp
else
return false
end
end
def string=(string)
@lexer.string = string
end
end
# $Id: parser_support.rb 2744 2007-08-04 20:00:19Z luke $
syntax highlighted by Code2HTML, v. 0.9.1