# vim: syntax=ruby # the parser class Puppet::Parser::Parser token LBRACK DQTEXT SQTEXT RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE token FALSE EQUALS LESSEQUAL NOTEQUAL DOT COLON TYPE LLCOLLECT RRCOLLECT token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSNAME CLASSREF token NOT OR AND # We have 2 shift/reduce conflicts #expect 2 rule program: statements { if val[0] # Make sure we always return an array. if val[0].is_a?(AST::ASTArray) if val[0].children.empty? result = nil else result = val[0] end else result = aryfy(val[0]) end else result = nil end } | nil statements: statement | statements statement { if val[0] and val[1] if val[0].instance_of?(AST::ASTArray) val[0].push(val[1]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[1]] end elsif obj = (val[0] || val[1]) result = obj else result = nil end } # The main list of valid statements statement: resource | virtualresource | collection | assignment | casestatement | ifstatement | import | fstatement | definition | hostclass | nodedef | resourceoverride fstatement: NAME LPAREN funcvalues RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :statement } | NAME funcvalues { args = aryfy(val[1]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } funcvalues: namestrings | resourcerefs namestrings: namestring | namestrings COMMA namestring { result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file } namestring: name | variable | quotedtext | CLASSNAME { result = ast AST::Name, :value => val[0] } resourcerefs: resourceref | resourcerefs COMMA resourceref { unless val[0].is_a?(AST::ASTArray) val[0] = aryfy(val[0]) end val[0].push(val[2]) result = val[0] } resource: fqname LBRACE resourceinstances endsemi RBRACE { array = val[2] if array.instance_of?(AST::ResourceInst) array = [array] end result = ast AST::ASTArray # this iterates across each specified resourceinstance array.each { |instance| unless instance.instance_of?(AST::ResourceInst) raise Puppet::Dev, "Got something that isn't an instance" end # now, i need to somehow differentiate between those things with # arrays in their names, and normal things result.push ast(AST::ResourceDef, :type => val[0], :title => instance[0], :params => instance[1]) } } | fqname LBRACE params endcomma RBRACE { # This is a deprecated syntax. error "All resource specifications require names" } | TYPE LBRACE params endcomma RBRACE { # a defaults setting for a type result = ast(AST::ResourceDefaults, :type => val[0], :params => val[2]) } # Override a value set elsewhere in the configuration. resourceoverride: resourceref LBRACE params endcomma RBRACE { result = ast AST::ResourceOverride, :object => val[0], :params => val[2] } # Exported and virtual resources; these don't get sent to the client # unless they get collected elsewhere in the db. virtualresource: at resource { type = val[0] if type == :exported and ! Puppet[:storeconfigs] error "You cannot collect without storeconfigs being set" end if val[1].is_a? AST::ResourceDefaults error "Defaults are not virtualizable" end method = type.to_s + "=" # Just mark our resources as exported and pass them through. if val[1].instance_of?(AST::ASTArray) val[1].each do |obj| obj.send(method, true) end else val[1].send(method, true) end result = val[1] } at: AT { result = :virtual } | AT AT { result = :exported } # A collection statement. Currently supports no arguments at all, but eventually # will, I assume. collection: collectname collectrhand { if val[0] =~ /^[a-z]/ Puppet.warning addcontext("Collection names must now be capitalized") end type = val[0].downcase args = {:type => type} if val[1].is_a?(AST::CollExpr) args[:query] = val[1] args[:query].type = type args[:form] = args[:query].form else args[:form] = val[1] end if args[:form] == :exported and ! Puppet[:storeconfigs] error "You cannot collect exported resources without storeconfigs being set" end result = ast AST::Collection, args } collectname: TYPE | NAME collectrhand: LCOLLECT collstatements RCOLLECT { if val[1] result = val[1] result.form = :virtual else result = :virtual end } | LLCOLLECT collstatements RRCOLLECT { if val[1] result = val[1] result.form = :exported else result = :exported end } # A mini-language for handling collection comparisons. This is organized # to avoid the need for precedence indications. collstatements: nil | collstatement | collstatements colljoin collstatement { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] } collstatement: collexpr | LPAREN collstatements RPAREN { result = val[1] result.parens = true } colljoin: AND | OR collexpr: colllval ISEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } | colllval NOTEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } colllval: variable | name resourceinst: resourcename COLON params endcomma { result = ast AST::ResourceInst, :children => [val[0],val[2]] } resourceinstances: resourceinst | resourceinstances SEMIC resourceinst { if val[0].instance_of?(AST::ResourceInst) result = ast AST::ASTArray, :children => [val[0],val[2]] else val[0].push val[2] result = val[0] end } endsemi: # nothing | SEMIC name: NAME { result = ast AST::Name, :value => val[0] } type: TYPE { result = ast AST::Type, :value => val[0] } resourcename: quotedtext | name | type | selector | variable | array assignment: VARIABLE EQUALS rvalue { # this is distinct from referencing a variable variable = ast AST::Name, :value => val[0] result = ast AST::VarDef, :name => variable, :value => val[2] } params: # nothing { result = ast AST::ASTArray } | param { result = val[0] } | params COMMA param { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } param: NAME FARROW rvalue { result = ast AST::ResourceParam, :param => val[0], :value => val[2] } rvalues: rvalue | rvalues comma rvalue { if val[0].instance_of?(AST::ASTArray) result = val[0].push(val[2]) else result = ast AST::ASTArray, :children => [val[0],val[2]] end } simplervalue: quotedtext | name | type | boolean | selector | variable rvalue: quotedtext | name | type | boolean | selector | variable | array | resourceref | funcrvalue # We currently require arguments in these functions. funcrvalue: NAME LPAREN namestrings RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :rvalue } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :rvalue } quotedtext: DQTEXT { result = ast AST::String, :value => val[0] } | SQTEXT { result = ast AST::FlatString, :value => val[0] } boolean: BOOLEAN { result = ast AST::Boolean, :value => val[0] } resourceref: NAME LBRACK rvalue RBRACK { Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") result = ast AST::ResourceRef, :type => val[0], :title => val[2] } | TYPE LBRACK rvalue RBRACK { result = ast AST::ResourceRef, :type => val[0], :title => val[2] } ifstatement: IF iftest LBRACE statements RBRACE else { args = { :test => val[1], :statements => val[3] } if val[5] args[:else] = val[5] end result = ast AST::IfStatement, args } else: # nothing | ELSE LBRACE statements RBRACE { result = ast AST::Else, :statements => val[2] } # Currently we only support a single value, but eventually one assumes # we'll support operators and such. iftest: rvalue casestatement: CASE rvalue LBRACE caseopts RBRACE { options = val[3] unless options.instance_of?(AST::ASTArray) options = ast AST::ASTArray, :children => [val[3]] end result = ast AST::CaseStatement, :test => val[1], :options => options } caseopts: caseopt | caseopts caseopt { if val[0].instance_of?(AST::ASTArray) val[0].push val[1] result = val[0] else result = ast AST::ASTArray, :children => [val[0], val[1]] end } caseopt: casevalues COLON LBRACE statements RBRACE { result = ast AST::CaseOpt, :value => val[0], :statements => val[3] } | casevalues COLON LBRACE RBRACE { result = ast(AST::CaseOpt, :value => val[0], :statements => ast(AST::ASTArray) ) } casevalues: selectlhand | casevalues COMMA selectlhand { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selector: selectlhand QMARK svalues { result = ast AST::Selector, :param => val[0], :values => val[2] } svalues: selectval | LBRACE sintvalues endcomma RBRACE { result = val[1] } sintvalues: selectval | sintvalues comma selectval { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selectval: selectlhand FARROW rvalue { result = ast AST::ResourceParam, :param => val[0], :value => val[2] } selectlhand: name | type | quotedtext | variable | funcrvalue | boolean | DEFAULT { result = ast AST::Default, :value => val[0] } import: IMPORT quotedtext { # importing files # yuk, i hate keywords # we'll probably have to have some kind of search path eventually # but for now, just use a path relative to the file doing the importing dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') 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 = val[1].value 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.each { |file| parser = Puppet::Parser::Parser.new(interp) 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::ImportError Puppet.warning( "Importing %s would result in an import loop" % File.join(dir, file) ) next end # This will normally add code to the 'main' class. parser.parse } } # Disable definition inheritance for now. 8/27/06, luke #definition: DEFINE NAME argumentlist parent LBRACE statements RBRACE { definition: DEFINE fqname argumentlist LBRACE statements RBRACE { interp.newdefine fqname(val[1]), :arguments => val[2], :code => val[4] @lexer.indefine = false result = nil #} | DEFINE NAME argumentlist parent LBRACE RBRACE { } | DEFINE fqname argumentlist LBRACE RBRACE { interp.newdefine fqname(val[1]), :arguments => val[2] @lexer.indefine = false result = nil } #hostclass: CLASS NAME argumentlist parent LBRACE statements RBRACE { hostclass: CLASS fqname classparent LBRACE statements RBRACE { # Our class gets defined in the parent namespace, not our own. @lexer.namepop interp.newclass fqname(val[1]), :code => val[4], :parent => val[2] result = nil } | CLASS fqname classparent LBRACE RBRACE { # Our class gets defined in the parent namespace, not our own. @lexer.namepop interp.newclass fqname(val[1]), :parent => val[2] result = nil } nodedef: NODE hostnames nodeparent LBRACE statements RBRACE { interp.newnode val[1], :parent => val[2], :code => val[4] result = nil } | NODE hostnames nodeparent LBRACE RBRACE { interp.newnode val[1], :parent => val[2] result = nil } fqname: NAME | CLASSNAME # Multiple hostnames, as used for node names. These are all literal # strings, not AST objects. hostnames: hostname | hostnames COMMA hostname { result = val[0] result = [result] unless result.is_a?(Array) result << val[2] } hostname: NAME | SQTEXT | DEFAULT nil: { result = nil } nothing: { result = ast AST::ASTArray, :children => [] } argumentlist: nil | LPAREN nothing RPAREN { result = nil } | LPAREN arguments RPAREN { result = val[1] result = [result] unless result[0].is_a?(Array) } arguments: argument | arguments COMMA argument { result = val[0] result = [result] unless result[0].is_a?(Array) result << val[2] } argument: NAME EQUALS rvalue { Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0], val[2]] } | NAME { Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0]] } | VARIABLE EQUALS rvalue { result = [val[0], val[2]] } | VARIABLE { result = [val[0]] } nodeparent: nil | INHERITS nameordefault { result = val[1] } classparent: nil | INHERITS fqnameordefault { result = val[1] } nameordefault: NAME | DEFAULT fqnameordefault: fqname | DEFAULT variable: VARIABLE { result = ast AST::Variable, :value => val[0] } array: LBRACK rvalues RBRACK { if val[1].instance_of?(AST::ASTArray) result = val[1] else result = ast AST::ASTArray, :children => [val[1]] end } | LBRACK RBRACK { result = ast AST::ASTArray } comma: FARROW | COMMA endcomma: # nothing | COMMA { result = nil } end ---- header ---- require 'puppet' require 'puppet/util/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' #require 'puppet/parser/interpreter' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end end Puppet[:typecheck] = true Puppet[:paramcheck] = true ---- inner ---- require 'puppet/parser/functions' 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[:line] hash[:line] = @lexer.line end unless hash[:file] if file = @lexer.file hash[:file] = file end end return klass.new(hash) end # Raise a Parse error. def error(message) 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::ImportError.new("Import loop detected") else @files << Puppet::Util::LoadedFile.new(file) @lexer.file = file end end def initialize(interpreter) @interp = interpreter initvars() end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] end # The fully qualifed name, with the full namespace. def fqname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') 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] 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. interp.newclass("", :code => main) return main end 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 # Make emacs happy # Local Variables: # mode: ruby # End: # $Id$