summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2005-04-13 15:24:36 +0000
committerLuke Kanies <luke@madstop.com>2005-04-13 15:24:36 +0000
commit6ee8b4e7a9731f969347e317456e8d9712fe2641 (patch)
tree0e2aa758bb523b4a2f1c752a625bd2d95462edf4
parent5416017c05e44fc635ad14ffdf1ac1163a4cc6e5 (diff)
downloadpuppet-6ee8b4e7a9731f969347e317456e8d9712fe2641.tar.gz
puppet-6ee8b4e7a9731f969347e317456e8d9712fe2641.tar.xz
puppet-6ee8b4e7a9731f969347e317456e8d9712fe2641.zip
reorganizing
git-svn-id: https://reductivelabs.com/svn/puppet/library/trunk@96 980ebf18-57e1-0310-9a29-db15c13687c0
-rw-r--r--lib/blink.rb234
-rwxr-xr-xlib/blink/attribute.rb137
-rwxr-xr-xlib/blink/component.rb55
-rw-r--r--lib/blink/fact.rb27
-rw-r--r--lib/blink/functions.rb41
-rw-r--r--lib/blink/interface.rb108
-rwxr-xr-xlib/blink/objects.rb431
-rwxr-xr-xlib/blink/objects/file.rb214
-rw-r--r--lib/blink/objects/package.rb197
-rwxr-xr-xlib/blink/objects/process.rb86
-rwxr-xr-xlib/blink/objects/service.rb136
-rw-r--r--lib/blink/objects/symlink.rb107
-rw-r--r--lib/blink/oparse.rb336
-rw-r--r--lib/blink/parser/grammar.ra451
-rw-r--r--lib/blink/parser/interpreter.rb222
-rw-r--r--lib/blink/parser/lexer.rb182
-rw-r--r--lib/blink/parser/makefile5
-rw-r--r--lib/blink/parser/parser.rb683
-rw-r--r--lib/blink/selector.rb82
19 files changed, 3734 insertions, 0 deletions
diff --git a/lib/blink.rb b/lib/blink.rb
new file mode 100644
index 000000000..70abb2a9c
--- /dev/null
+++ b/lib/blink.rb
@@ -0,0 +1,234 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'digest/md5'
+require 'etc'
+require 'singleton'
+require 'blink/component'
+require 'blink/interface'
+require 'blink/selector'
+require 'blink/objects'
+require 'blink/objects/service'
+require 'blink/objects/file'
+require 'blink/objects/symlink'
+
+PINK=""
+GREEN=""
+YELLOW=""
+SLATE=""
+ORANGE=""
+BLUE=""
+RESET=""
+
+#------------------------------------------------------------
+# the top-level module
+#
+# all this really does is dictate how the whole system behaves, through
+# preferences for things like debugging
+#
+# it's also a place to find top-level commands like 'debug'
+module Blink
+ # the hash that determines how our system behaves
+ @@config = Hash.new(false)
+
+ # produce debugging info
+ def Blink.debug(ary)
+ msg = ""
+ if ary.class == String
+ msg = ary
+ else
+ msg = ary.join(" ")
+ end
+
+ if @@config[:debug]
+ Blink::Message.new(
+ :level => :debug,
+ :source => "Blink",
+ :message => msg
+ )
+ end
+ end
+
+ # set up our configuration
+ def Blink.init(args)
+ args.each {|p,v|
+ @@config[p] = v
+ }
+ end
+
+ # just print any messages we get
+ # we should later behave differently depending on the message
+ def Blink.newmessage(msg)
+ puts msg
+ end
+
+ # DISABLED
+
+ # we've collected all data; now operate on it
+# def Blink.run
+# ops = Blink::Objects.genops()
+# ops.find_all { |op|
+# op.auto?()
+# }.each { |op|
+# Blink::Message.new(
+# :level => :debug,
+# :source => "Blink",
+# :message => "Running op %s" % op
+# )
+# op.check
+# }.find_all { |op|
+# puts "dirty? #{op}"
+# op.dirty?
+# }.collect { |op|
+# puts "%s is dirty; %s instead of %s" % [op, op.state, op.should]
+# op.fix
+# }.each { |event| # this might need to support lists someday...
+# #list.each { |event|
+# puts event
+# event.trigger
+# #}
+# }
+# end
+#
+# def Blink.walk
+# root = Blink::Objects.root
+# root.check
+# if root.dirty?
+# Blink::Message.new(
+# :message => "someone's dirty",
+# :level => :notice,
+# :source => root
+# )
+# root.fix
+# end
+# end
+
+ # configuration parameter access and stuff
+ def Blink.[](param)
+ return @@config[param]
+ end
+
+ # configuration parameter access and stuff
+ def Blink.[]=(param,value)
+ @@config[param] = value
+ end
+
+ # a simple class for creating callbacks
+ class Event
+ attr_reader :event, :object
+ attr_writer :event, :object
+
+ def initialize(args)
+ @event = args[:event]
+ @object = args[:object]
+
+ if @event.nil? or @object.nil?
+ raise "Event.new called incorrectly"
+ end
+ end
+
+ def trigger
+ @object.trigger(@event)
+ end
+ end
+
+ # a class for storing state
+ # not currently used
+ class State
+ include Singleton
+ @@config = "/var/tmp/blinkstate"
+ @@state = Hash.new
+ @@splitchar = " "
+
+ def initialize
+ self.load
+ end
+
+ def State.load
+ puts "loading state"
+ return unless File.exists?(@@config)
+ File.open(@@config) { |file|
+ file.gets { |line|
+ myclass, key, value = line.split(@@splitchar)
+
+ unless defined? @@state[myclass]
+ @@state[myclass] = Hash.new
+ end
+
+ @@state[myclass][key] = value
+ }
+ }
+ end
+
+ def State.state(myclass)
+ unless defined? @@state[myclass]
+ @@state[myclass] = Hash.new
+ end
+ return @@state[myclass]
+ end
+
+ def State.store
+ File.open(@@config, File::CREAT|File::WRONLY, 0644) { |file|
+ @@state.each { |key, value|
+ file.puts([self.class,key,value].join(@@splitchar))
+ }
+ }
+ end
+ end
+
+ #------------------------------------------------------------
+ # provide feedback of various types to the user
+ # modeled after syslog messages
+ # each level of message prints in a different color
+ class Message
+ @@messages = Array.new
+ @@levels = [ :debug, :verbose, :notice, :warning, :error ]
+ @@colors = {
+ :debug => SLATE,
+ :verbose => ORANGE,
+ :notice => PINK,
+ :warning => GREEN,
+ :error => YELLOW
+ }
+
+ attr_reader :level, :message
+ attr_writer :level, :message
+
+ def initialize(args)
+ unless args.include?(:level) && args.include?(:message) &&
+ args.include?(:source)
+ raise "Blink::Message called incorrectly"
+ end
+
+ if args[:level].class == String
+ @level = args[:level].intern
+ elsif args[:level].class == Symbol
+ @level = args[:level]
+ else
+ raise "Level is not a string or symbol: #{args[:level].class}"
+ end
+ @message = args[:message]
+ @source = args[:source]
+ @time = Time.now
+ # this should include the host name, and probly lots of other
+ # stuff, at some point
+ unless @@levels.include?(level)
+ raise "Invalid message level #{level}"
+ end
+
+ @@messages.push(self)
+ Blink.newmessage(self)
+ end
+
+ def to_s
+ # this probably won't stay, but until this leaves the console,
+ # i'm going to use coloring...
+ #return "#{@time} #{@source} (#{@level}): #{@message}"
+ return @@colors[@level] + "%s %s (%s): %s" % [
+ @time, @source, @level, @message
+ ] + RESET
+ end
+ end
+ #------------------------------------------------------------
+end
diff --git a/lib/blink/attribute.rb b/lib/blink/attribute.rb
new file mode 100755
index 000000000..e6c2d83bd
--- /dev/null
+++ b/lib/blink/attribute.rb
@@ -0,0 +1,137 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+module Blink
+ # this is a virtual base class for attributes
+ # attributes are self-contained building blocks for objects
+
+ # Attributes can currently only be used for comparing a virtual "should" value
+ # against the real state of the system. For instance, you could verify that
+ # a file's owner is what you want, but you could not create two file objects
+ # and use these methods to verify that they have the same owner
+ class Attribute
+ include Comparable
+
+ attr_accessor :value
+ attr_accessor :should
+ attr_accessor :object
+
+ #-----------------------------------
+ # every attribute class must tell us what it's name will be (as a symbol)
+ # this determines how we will refer to the attribute during usage
+ # e.g., the Owner attribute for Files might say its name is :owner;
+ # this means that we can say "file[:owner] = 'yayness'"
+ def Attribute.name
+ return @name
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # we aren't actually comparing the attributes themselves, we're only
+ # comparing the "should" value with the "real" value
+ def insync?
+ Blink.debug "%s value is %s, should be %s" % [self,self.value,self.should]
+ self.value == self.should
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def initialize(value)
+ @should = value
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # DISABLED: we aren't comparing attributes, just attribute values
+ # are we in sync?
+ # this could be a comparison between two attributes on two objects,
+ # or a comparison between an object and the live system -- we'll
+ # let the object decide that, rather than us
+ #def <=>(other)
+ # if (self.value.respond_to?(:<=>))
+ # return self.value <=> other
+ # else
+ # fail TypeError.new("class #{self.value.class} does not respond to <=>")
+ # end
+ #end
+ #-----------------------------------
+
+ #-----------------------------------
+ # each attribute class must define the name() method
+ def name
+ return self.class.name
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # retrieve the current state from the running system
+ def retrieve
+ raise "'retrieve' method was not overridden by %s" % self.class
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def to_s
+ return @object.name.to_s + " -> " + self.name.to_s
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # this class is for attributes that don't reflect on disk,
+ # like 'path' on files and 'name' on processes
+ # these are basically organizational attributes, not functional ones
+ #
+ # we provide stub methods, so that from the outside it looks like
+ # other attributes
+ #
+ # see objects.rb for how this is used
+ class Symbol
+ attr_reader :value
+ attr_reader :should
+
+ def initialize(symbol)
+ @symbol = symbol
+ end
+
+ def name
+ return @symbol.id2name
+ end
+
+ def retrieve
+ true
+ end
+
+ def insync?
+ true
+ end
+
+ def should=(value)
+ @value = value
+ @should = value
+ end
+
+ def sync
+ true
+ end
+
+ def value=(value)
+ @value = value
+ @should = value
+ end
+ end
+ end
+
+ # this class is supposed to be used to solve cases like file modes,
+ # where one command (stat) retrieves enough data to cover many attributes
+ # (e.g., setuid, setgid, world-read, etc.)
+ class MetaAttribute
+ include Comparable
+ attr_accessor :parent
+ attr_accessor :value
+
+ def <=>(other)
+ raise "'<=>' method was not overridden by %s" % self.class
+ end
+ end
+end
diff --git a/lib/blink/component.rb b/lib/blink/component.rb
new file mode 100755
index 000000000..bad1b0761
--- /dev/null
+++ b/lib/blink/component.rb
@@ -0,0 +1,55 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the object allowing us to build complex structures
+# this thing contains everything else, including itself
+
+require 'blink/interface'
+
+module Blink
+ class Component < Blink::Interface
+ attr_accessor :name
+
+ @objects = Hash.new(nil)
+
+ #---------------------------------------------------------------
+ # our components are effectively arrays, with a bit extra functionality
+ def each
+ @subobjects.each { |obj|
+ yield obj
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def initialize(*args)
+ args = Hash[*args]
+
+ unless args.include?(:name)
+ fail TypeError.new("Components must be provided a name")
+ else
+ self.name = args[:name]
+ end
+
+ Component[self.name] = self
+
+ @subobjects = []
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def push(*objs)
+ objs.each { |obj|
+ @subobjects.push(obj)
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ return self.name
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/blink/fact.rb b/lib/blink/fact.rb
new file mode 100644
index 000000000..9adcc3b7a
--- /dev/null
+++ b/lib/blink/fact.rb
@@ -0,0 +1,27 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# an interface for registering and retrieving facts
+# this is an abstract interface, and should just be used to interact
+# with another library
+
+# currently a very thin veneer on 'facter'
+
+require 'facter'
+
+module Blink
+ class Fact
+ # just pass the block to 'add'
+ # the block has to do things like set the interpreter,
+ # the code (which can be a ruby block), and maybe the
+ # os and osrelease
+ def Fact.add(name,&block)
+ Facter[name].add(&block)
+ end
+
+ def Fact.[](name)
+ Facter[name]
+ end
+ end
+end
diff --git a/lib/blink/functions.rb b/lib/blink/functions.rb
new file mode 100644
index 000000000..a352cfa4b
--- /dev/null
+++ b/lib/blink/functions.rb
@@ -0,0 +1,41 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+require 'blink/fact'
+
+module Blink
+ class Function
+ @@functions = Hash.new(nil)
+
+ #---------------------------------------------------------------
+ def [](name)
+ return @@functions[name]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def call(*args)
+ @code(*args)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # we want a 'proc' item instead of a block, so that we can return
+ # from it
+ def initialize(name,code)
+ @name = name
+ @code = code
+
+ @@functions[name] = self
+ end
+ #---------------------------------------------------------------
+ end
+
+ Function.new("retrieve", proc { |fact|
+ require 'blink/fact'
+
+ return Fact[fact]
+ })
+end
diff --git a/lib/blink/interface.rb b/lib/blink/interface.rb
new file mode 100644
index 000000000..a1496f01b
--- /dev/null
+++ b/lib/blink/interface.rb
@@ -0,0 +1,108 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# our duck type interface
+
+# all of our first-class objects (objects, attributes, and components) will
+# respond to these methods
+# although attributes don't inherit from Blink::Interface
+# although maybe Blink::Attribute should...
+
+# the default behaviour that this class provides is to just call a given
+# method on each contained object, e.g., in calling 'sync', we just run:
+# object.each { |subobj| subobj.sync() }
+
+# to use this interface, just define an 'each' method and 'include Blink::Interface'
+module Blink
+ class Interface
+ # each subclass of Blink::Interface must create a class-local @objects
+ # variable
+ @objects = Hash.new # this one won't be used, since this class is abstract
+
+ # this is a bit of a hack, but it'll work for now
+ attr_accessor :performoperation
+
+ #---------------------------------------------------------------
+ def evaluate
+ self.retrieve
+ unless self.insync?
+ if @performoperation == :sync
+ self.sync
+ else
+ # we, uh, don't do anything
+ end
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # set up the "interface" methods
+ [:sync,:retrieve].each { |method|
+ self.send(:define_method,method) {
+ self.each { |subobj|
+ Blink.debug("sending '%s' to '%s'" % [method,subobj])
+ subobj.send(method)
+ }
+ }
+ }
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # if all contained objects are in sync, then we're in sync
+ def insync?
+ insync = true
+
+ self.each { |obj|
+ unless obj.insync?
+ Blink.debug("%s is not in sync" % obj)
+ insync = false
+ end
+ }
+
+ Blink.debug("%s sync status is %s" % [self,insync])
+ return insync
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # retrieve a named object
+ def Interface.[](name)
+ if @objects.has_key?(name)
+ return @objects[name]
+ else
+ raise "Object '#{name}' does not exist"
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is special, because it can be equivalent to running new
+ # this allows cool syntax like Blink::File["/etc/inetd.conf"] = ...
+ def Interface.[]=(name,object)
+ newobj = nil
+ if object.is_a?(Blink::Interface)
+ newobj = object
+ else
+ raise "must pass a Blink::Interface object"
+ end
+
+ if @objects.has_key?(newobj.name)
+ puts @objects
+ raise "'#{newobj.name}' already exists in " +
+ "class '#{newobj.class}': #{@objects[newobj.name]}"
+ else
+ Blink.debug("adding %s of type %s to class list" %
+ [object.name,object.class])
+ @objects[newobj.name] = newobj
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Interface.has_key?(name)
+ return @objects.has_key?(name)
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/blink/objects.rb b/lib/blink/objects.rb
new file mode 100755
index 000000000..e00b7ac7c
--- /dev/null
+++ b/lib/blink/objects.rb
@@ -0,0 +1,431 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink/attribute'
+require 'blink/interface'
+
+
+module Blink
+ class Objects < Blink::Interface
+ include Enumerable
+ @objects = Hash.new # a class instance variable
+ @@allobjects = Array.new # and then a hash for all objects
+
+ #---------------------------------------------------------------
+ # the class methods
+
+ #-----------------------------------
+ # all objects total
+ def Objects.push(object)
+ @@allobjects.push object
+ Blink.debug("adding %s of type %s to master list" % [object.name,object.class])
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # this should make it so our subclasses don't have to worry about
+ # defining these class instance variables
+ def Objects.inherited(sub)
+ sub.module_eval %q{
+ @objects = Hash.new
+ @actions = Hash.new
+ }
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # some simple stuff to make it easier to get a name from everyone
+ def Objects.namevar
+ return @namevar
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # accessor for the list of acceptable params
+ def Objects.classparams
+ return @params
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # our param list is by class, so we need to convert it to names
+ # (see blink/objects/file.rb for an example of how params are defined)
+ def Objects.classparambyname
+ unless defined? @paramsbyname
+ @paramsbyname = Hash.new { |hash,key|
+ fail TypeError.new(
+ "Parameter %s is invalid for class %s" %
+ [key.to_s,self.class.to_s]
+ )
+ }
+ @params.each { |param|
+ if param.is_a? Symbol
+ # store the Symbol class, not the symbol itself
+ symbolattr = Blink::Attribute::Symbol.new(param)
+
+ @paramsbyname[param] = symbolattr
+ elsif param.respond_to?(:name)
+ # these are already classes
+ @paramsbyname[param.name] = param
+ else
+ fail TypeError.new(
+ "Parameter %s is invalid; it must be a class or symbol" %
+ param.to_s
+ )
+ end
+ }
+ end
+ return @paramsbyname
+ end
+ #-----------------------------------
+
+ #---------------------------------------------------------------
+ # the instance methods
+
+ #-----------------------------------
+ # parameter access and stuff
+ def [](param)
+ if @attributes.has_key?(param)
+ return @attributes[param].should
+ else
+ raise "Undefined parameter '#{param}' in #{self}"
+ end
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # because all object parameters are actually attributes, we
+ # have to do some shenanigans to make it look from the outside
+ # like @attributes is just a simple hash
+ # the Symbol stuff is especially a bit hackish
+ def []=(param,value)
+ if @attributes.has_key?(param)
+ @attributes[param].should = value
+ return
+ end
+
+ attrclass = self.class.classparambyname[param]
+
+ Blink.debug("creating attribute of type '%s'" % attrclass)
+ # any given object can normally only have one of any given attribute
+ # type, but it might have many Symbol attributes
+ #
+ # so, we need to make sure that the @attributes hash behaves
+ # the same whether it has a unique attribute or a bunch of Symbol
+ # attributes
+ if attrclass.is_a?(Blink::Attribute::Symbol)
+ attrclass.should = value
+ @attributes[param] = attrclass
+ else
+ attr = attrclass.new(value)
+ attr.object = self
+ if attr.is_a?(Array)
+ attr.each { |xattr|
+ @attributes[xattr.name] = attr
+ }
+ else
+ Blink.debug "Creating attr %s in %s" % [attr.name,self]
+ @attributes[attr.name] = attr
+ end
+ end
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # return action array
+ # these are actions to use for responding to events
+ # no, this probably isn't the best way, because we're providing
+ # access to the actual hash, which is silly
+ def action
+ if not defined? @actions
+ puts "defining action hash"
+ @actions = Hash.new
+ end
+ @actions
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # removing attributes
+ def delete(attr)
+ if @attributes.has_key?(attr)
+ @attributes.delete(attr)
+ else
+ raise "Undefined attribute '#{attr}' in #{self}"
+ end
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # XXX this won't work -- too simplistic
+ # a given object can be in multiple components
+ # which means... what? that i have to delete things from components?
+ # that doesn't seem right, somehow...
+ # do i really ever need to delete things?
+ #def delete
+ # self.class.delete[self.name]
+ #end
+ #-----------------------------------
+
+ #-----------------------------------
+ # this can only be used with blocks that are
+ # valid on operations and objects, as it iterates over both of
+ # them
+ # essentially, the interface defined by Blink::Interface is used here
+ def each
+ ret = false
+ nodepth = 0
+ unless block_given?
+ raise "'Each' was not given a block"
+ end
+ @attributes.each { |name,attr|
+ #Blink.debug "'%s' yielding '%s' of type '%s'" % [self,attr,attr.class]
+ yield(attr)
+ }
+ # DISABLED
+ # until we're clear on what 'enclosure' means, this is
+ # all disabled
+
+ #if @encloses.length > 0
+ # Blink.debug "#{self} encloses #{@encloses}"
+ ##end
+ #if defined? Blink['depthfirst']
+ # self.eachobj { |enclosed|
+ # Blink.debug "yielding #{self} to object #{enclosed}"
+ # ret |= yield(enclosed)
+ # }
+ # nodepth = 1
+ #end
+ #self.eachop { |op|
+ # Blink.debug "yielding #{self} to op #{op}"
+ # ret |= yield(op)
+ #}
+ #if ! defined? Blink['depthfirst'] and nodepth != 1
+ # self.eachobj { |enclosed|
+ # Blink.debug "yielding #{self} to object #{enclosed}"
+ # ret |= yield(enclosed)
+ # }
+ #end
+ #return ret
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # this allows each object to act like both a node and
+ # a branch
+ # but each object contains two types of objects: operations and other
+ # objects....
+ def eachobj
+ unless block_given?
+ raise "Eachobj was not given a block"
+ end
+ @encloses.each { |object|
+ yield(object)
+ }
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # store the object that immediately encloses us
+ def enclosedby(obj)
+ @enclosedby.push(obj)
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def enclosed?
+ defined? @enclosedby
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # store a enclosed object
+ def encloses(obj)
+ obj.enclosedby(self)
+ #obj.subscribe(self,'*')
+ @encloses.push(obj)
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # this is a wrapper, doing all of the work that should be done
+ # and none that shouldn't
+ def evaluate
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # call an event
+ # this is called on subscribers by the trigger method from the obj
+ # which sent the event
+ # event handling should probably be taking place in a central process,
+ # but....
+ def event(event,obj)
+ Blink.debug "#{self} got event #{event} from #{obj}"
+ if @actions.key?(event)
+ Blink.debug "calling it"
+ @actions[event].call(self,obj,event)
+ else
+ p @actions
+ end
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # yay
+ def initialize(*args)
+ # params are for classes, attributes are for instances
+ # hokey but true
+ @attributes = Hash.new
+ @monitor = Array.new
+
+ # default to always syncing
+ @performoperation = :sync
+
+ begin
+ hash = Hash[*args]
+ rescue ArgumentError
+ fail TypeError.new("Incorrect number of arguments for %s" %
+ self.class.to_s)
+ end
+
+ # if they passed in a list of attributes they're interested in,
+ # we mark them as "interesting"
+ # XXX maybe we should just consider params set to nil as 'interesting'
+ #
+ # this isn't used as much as it should be, but the idea is that
+ # the "interesting" attributes would be the ones retrieved during a
+ # 'retrieve' call
+ if hash.include?(:check)
+ @monitor = hash[:check].dup
+ hash.delete(:check)
+ end
+
+ # we have to set the name of our object before anything else,
+ # because it might be used in creating the other attributes
+ if hash.has_key?(self.class.namevar)
+ self[self.class.namevar] = hash[self.class.namevar]
+ hash.delete(self.class.namevar)
+ else
+ raise TypeError.new("A name must be provided at initialization time")
+ end
+
+ hash.each { |param,value|
+ @monitor.push(param)
+ Blink.debug("adding param '%s' with value '%s'" %
+ [param,value])
+ self[param] = value
+ }
+
+ # add this object to the specific class's list of objects
+ self.class[name] = self
+
+ # and then add it to the master list
+ Blink::Objects.push(self)
+
+ @notify = Hash.new
+ #@encloses = Array.new
+ #@enclosedby = Array.new
+ @actions = Hash.new
+ #@opsgenned = false
+
+ # XXX i've no idea wtf is going on with enclosures
+ #if self.class == Blink::Objects::Root
+ # Blink.debug "not enclosing root (#{self.class}) in self"
+ #else
+ # Blink::Objects.root.encloses(self)
+ #end
+ end
+ # initialize
+ #-----------------------------------
+
+ #-----------------------------------
+ def name
+ #namevar = self.class.namevar
+ #Blink.debug "namevar is '%s'" % namevar
+ #nameattr = @attributes[namevar]
+ #Blink.debug "nameattr is '%s'" % nameattr
+ #name = nameattr.value
+ #Blink.debug "returning %s from attr %s and namevar %s" % [name,nameattr,namevar]
+ #return name
+ return @attributes[self.class.namevar].value
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def newevent(args)
+ if args[:event].nil?
+ raise "newevent called wrong on #{self}"
+ end
+
+ return Blink::Event.new(
+ :event => args[:event],
+ :object => self
+ )
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # subscribe to an event or all events
+ # this entire event system is a hack job and needs to
+ # be replaced with a central event handler
+ def subscribe(args,&block)
+ obj = args[:object]
+ event = args[:event] || '*'.intern
+ if obj.nil? or event.nil?
+ raise "subscribe was called wrongly; #{obj} #{event}"
+ end
+ obj.action[event] = block
+ #events.each { |event|
+ unless @notify.key?(event)
+ @notify[event] = Array.new
+ end
+ unless @notify[event].include?(obj)
+ Blink.debug "pushing event '%s' for object '%s'" % [event,obj]
+ @notify[event].push(obj)
+ end
+ # }
+ #else
+ # @notify['*'.intern].push(obj)
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def to_s
+ self.name
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ # initiate a response to an event
+ def trigger(event)
+ subscribers = Array.new
+ if @notify.include?('*') and @notify['*'].length > 0
+ @notify['*'].each { |obj| subscribers.push(obj) }
+ end
+ if (@notify.include?(event) and (! @notify[event].empty?) )
+ @notify[event].each { |obj| subscribers.push(obj) }
+ end
+ Blink.debug "triggering #{event}"
+ subscribers.each { |obj|
+ Blink.debug "calling #{event} on #{obj}"
+ obj.event(event,self)
+ }
+ end
+ #-----------------------------------
+
+ #-----------------------------------
+ def validparam(param)
+ if (self.class.operparams.include?(param) or
+ self.class.staticparams.include?(param))
+ return true
+ else
+ return false
+ end
+ end
+ #-----------------------------------
+
+ #---------------------------------------------------------------
+ end # Blink::Objects
+end
diff --git a/lib/blink/objects/file.rb b/lib/blink/objects/file.rb
new file mode 100755
index 000000000..3f2a8dcfc
--- /dev/null
+++ b/lib/blink/objects/file.rb
@@ -0,0 +1,214 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'digest/md5'
+require 'etc'
+require 'blink/attribute'
+
+module Blink
+ # we first define all of the attribute that our file will use
+ # because the objects must be defined for us to use them in our
+ # definition of the file object
+ class Attribute
+ class FileUID < Blink::Attribute
+ require 'etc'
+ attr_accessor :file
+ @name = :owner
+
+ def retrieve
+ stat = nil
+
+ begin
+ stat = File.stat(self.object[:path])
+ rescue
+ # this isn't correct, but what the hell
+ raise "File '%s' does not exist: #{$!}" % self.object[:path]
+ end
+
+ self.value = stat.uid
+ unless self.should.is_a?(Integer)
+ begin
+ user = Etc.getpwnam(self.should)
+ if user.gid == ""
+ raise "Could not retrieve uid for %s" % self.object
+ end
+ Blink.debug "converting %s to integer %d" %
+ [self.should,user.uid]
+ self.should = user.uid
+ rescue
+ raise "Could not get any info on user %s" % self.should
+ end
+ end
+ Blink.debug "chown state is %d" % self.value
+ end
+
+ #def <=>(other)
+ # if other.is_a?(Integer)
+ # begin
+ # other = Etc.getpwnam(other).uid
+ # rescue
+ # raise "Could not get uid for #{@params[:uid]}"
+ # end
+ # end
+#
+# self.value <=> other
+# end
+
+ def sync
+ begin
+ File.chown(value,-1,self.object[:path])
+ rescue
+ raise "failed to sync #{@params[:file]}: #{$!}"
+ end
+
+ self.object.newevent(:event => :inode_changed)
+ end
+ end
+
+ # this attribute should actually somehow turn into many attributes,
+ # one for each bit in the mode
+ # I think MetaAttributes are the answer, but I'm not quite sure
+ class FileMode < Blink::Attribute
+ require 'etc'
+
+ @name = :mode
+
+ def retrieve
+ stat = nil
+
+ begin
+ stat = File.stat(self.object[:path])
+ rescue => error
+ raise "File %s could not be stat'ed: %s" % [self.object[:path],error]
+ end
+
+ self.value = stat.mode & 007777
+ Blink.debug "chmod state is %o" % self.value
+ end
+
+ def sync
+ begin
+ File.chmod(self.should,self.object[:path])
+ rescue
+ raise "failed to chmod #{self.object[:path]}: #{$!}"
+ end
+ self.object.newevent(:event => :inode_changed)
+ end
+ end
+
+ # not used until I can figure out how to solve the problem with
+ # metaattributes
+ class FileSetUID < Blink::Attribute
+ require 'etc'
+
+ @parent = Blink::Attribute::FileMode
+
+ @name = :setuid
+
+ def <=>(other)
+ self.value <=> @parent.value[11]
+ end
+
+ # this just doesn't seem right...
+ def sync
+ tmp = 0
+ if self.value == true
+ tmp = 1
+ end
+ @parent.value[11] = tmp
+ end
+ end
+
+ class FileGroup < Blink::Attribute
+ require 'etc'
+
+ @name = :group
+
+ def retrieve
+ stat = nil
+
+ begin
+ stat = File.stat(self.object[:path])
+ rescue
+ # this isn't correct, but what the hell
+ raise "File #{self.object[:path]} does not exist: #{$!}"
+ end
+
+ self.value = stat.gid
+
+ # we probably shouldn't actually modify the 'should' value
+ # but i don't see a good way around it right now
+ # mmmm, should
+ unless self.should.is_a?(Integer)
+ begin
+ group = Etc.getgrnam(self.should)
+ # yeah, don't ask me
+ # this is retarded
+ #p group
+ if group.gid == ""
+ raise "Could not retrieve gid for %s" % self.object
+ end
+ Blink.debug "converting %s to integer %d" %
+ [self.should,group.gid]
+ self.should = group.gid
+ rescue
+ raise "Could not get any info on group %s" % self.should
+ end
+ end
+ Blink.debug "chgrp state is %d" % self.value
+ end
+
+# def <=>(other)
+# # unless we're numeric...
+# if other.is_a?(Integer)
+# begin
+# group = Etc.getgrnam(other)
+# # yeah, don't ask me
+# # this is retarded
+# #p group
+# other = group.gid
+# if other == ""
+# raise "Could not retrieve gid for %s" % other
+# end
+# rescue
+# raise "Could not get any info on group %s" % other
+# end
+# end
+#
+# #puts self.should
+# self.value <=> other
+# end
+
+ def sync
+ Blink.debug "setting chgrp state to %d" % self.should
+ begin
+ # set owner to nil so it's ignored
+ File.chown(nil,self.should,self.object[:path])
+ rescue
+ raise "failed to chgrp %s to %s: %s" %
+ [self.object[:path], self.should, $!]
+ end
+ self.object.newevent(:event => :inode_changed)
+ end
+ end
+ end
+ class Objects
+ class File < Objects
+ attr_reader :stat, :path, :params
+ # class instance variable
+ @params = [
+ Blink::Attribute::FileUID,
+ Blink::Attribute::FileGroup,
+ Blink::Attribute::FileMode,
+ Blink::Attribute::FileSetUID,
+ :path
+ ]
+
+ @objects = Hash.new
+ @actions = Hash.new
+ @namevar = :path
+ end # Blink::Objects::File
+ end # Blink::Objects
+
+end
diff --git a/lib/blink/objects/package.rb b/lib/blink/objects/package.rb
new file mode 100644
index 000000000..f35368022
--- /dev/null
+++ b/lib/blink/objects/package.rb
@@ -0,0 +1,197 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+module Blink
+ class Objects
+ # packages are complicated because each package format has completely
+ # different commands. We need some way to convert specific packages
+ # into the general package object...
+ class Package < Objects
+ attr_reader :version, :format
+ # class instance variable
+ @params = [
+ :install,
+ :format,
+ :version
+ ]
+
+ @namevar = :name
+
+ # this is already being done in objects.rb
+ #def Package.inhereted(sub)
+ # sub.module_eval %q{
+ # @objects = Hash.new
+ # @actions = Hash.new
+ # }
+ #end
+
+ def initialize(hash)
+ end
+
+ def retrieve
+ end
+
+ def insync?
+ end
+
+ def sync
+ end
+ end # Blink::Objects::Package
+
+ class PackagingType
+ attr_writer :list, :install, :remove, :check
+
+ @@types = Hash.new(false)
+
+ def PackagingType.[](name)
+ if @@types.include?(name)
+ return @@types[name]
+ else
+ raise "no such type %s" % name
+ end
+ end
+
+ [:list, :install, :remove, :check].each { |method|
+ self.send(:define_method, method) {
+ # retrieve the variable
+ var = eval("@" + method.id2name)
+ if var.is_a?(Proc)
+ var.call()
+ else
+ raise "only blocks are supported right now"
+ end
+ }
+ }
+
+ def initialize(name)
+ if block_given?
+ yield self
+ end
+
+ @packages = Hash.new(false)
+ @@types[name] = self
+ end
+
+ def retrieve
+ @packages.clear()
+
+ @packages = self.list()
+ end
+ end
+
+ PackagingType.new("dpkg") { |type|
+ type.list = proc {
+ packages = []
+
+ # dpkg only prints as many columns as you have available
+ # which means we don't get all of the info
+ # stupid stupid
+ oldcol = ENV["COLUMNS"]
+ ENV["COLUMNS"] = "500"
+
+ # list out all of the packages
+ open("| dpkg -l") { |process|
+ # our regex for matching dpkg output
+ regex = %r{^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$}
+ fields = [:status, :name, :version, :description]
+ hash = {}
+
+ 5.times { process.gets } # throw away the header
+
+ # now turn each returned line into a package object
+ process.each { |line|
+ if match = regex.match(line)
+ hash.clear
+
+ fields.zip(match.captures) { |field,value|
+ hash[field] = value
+ }
+ packages.push Blink::Objects::Package.new(hash)
+ else
+ raise "failed to match dpkg line %s" % line
+ end
+ }
+ }
+ ENV["COLUMNS"] = oldcol
+
+ return packages
+ }
+
+ # we need package retrieval mechanisms before we can have package
+ # installation mechanisms...
+ #type.install = proc { |pkg|
+ # raise "installation not implemented yet"
+ #}
+
+ type.remove = proc { |pkg|
+ cmd = "dpkg -r %s" % pkg.name
+ output = %x{#{cmd}}
+ if $? != 0
+ raise output
+ end
+ }
+ }
+
+ PackagingType.new("sunpkg") { |type|
+ type.list = proc {
+ packages = []
+ hash = {}
+ names = {
+ "PKGINST" => :name,
+ "NAME" => nil,
+ "CATEGORY" => :category,
+ "ARCH" => :platform,
+ "VERSION" => :version,
+ "BASEDIR" => :root,
+ "HOTLINE" => nil,
+ "EMAIL" => nil,
+ "VENDOR" => :vendor,
+ "DESC" => :description,
+ "PSTAMP" => nil,
+ "INSTDATE" => nil,
+ "STATUS" => nil,
+ "FILES" => nil
+ }
+
+ # list out all of the packages
+ open("| pkginfo -l") { |process|
+ # we're using the long listing, so each line is a separate piece
+ # of information
+ process.each { |line|
+ case line
+ when /^$/ then
+ packages.push Blink::Objects::Package.new(hash)
+ hash.clear
+ when /\s*(\w+):\s+(.+)/
+ name = $1
+ value = $2
+ if names.include?(name)
+ hash[names[name]] = value
+ else
+ raise "Could not find %s" % name
+ end
+ when /\s+\d+.+/
+ # nothing; we're ignoring the FILES info
+ end
+ }
+ }
+ return packages
+ }
+
+ # we need package retrieval mechanisms before we can have package
+ # installation mechanisms...
+ #type.install = proc { |pkg|
+ # raise "installation not implemented yet"
+ #}
+
+ type.remove = proc { |pkg|
+ cmd = "pkgrm -n %s" % pkg.name
+ output = %x{#{cmd}}
+ if $? != 0
+ raise output
+ end
+ }
+ }
+ end # Blink::Objects
+end
diff --git a/lib/blink/objects/process.rb b/lib/blink/objects/process.rb
new file mode 100755
index 000000000..182667d7c
--- /dev/null
+++ b/lib/blink/objects/process.rb
@@ -0,0 +1,86 @@
+#!/usr/local/bin/ruby -w
+
+require 'blink/operation'
+require 'blink/operation/processes'
+
+# DISABLED
+# I'm only working on services, not processes, right now
+
+module Blink
+ class Objects
+ class BProcess < Objects
+ attr_reader :stat, :path
+ @params = [:start, :stop, :user, :pattern, :binary, :arguments] # class instance variable
+
+ @objects = Hash.new
+ @namevar = :pattern
+
+ Blink::Relation.new(self, Blink::Operation::Start, {
+ :user => :user,
+ :pattern => :pattern,
+ :binary => :binary,
+ :arguments => :arguments
+ })
+
+ Blink::Relation.new(self, Blink::Operation::Stop, {
+ :user => :user,
+ :pattern => :pattern
+ })
+
+ end # Blink::Objects::BProcess
+ end # Blink::Objects
+
+ class Attribute
+ class ProcessRunning < Attribute
+ def retrieve
+ running = 0
+ regex = Regexp.new(@params[:pattern])
+ begin
+ # this ps is only tested on Solaris
+ # XXX yeah, this definitely needs to be fixed...
+ %x{ps -ef -U #{@params[:user]}}.split("\n").each { |process|
+ if regex.match(process)
+ running += 1
+ end
+ }
+ rescue
+ # this isn't correct, but what the hell
+ Blink::Message.new(
+ :level => :error,
+ :source => self.object,
+ :message => "Failed to run ps"
+ )
+ end
+
+ self.state = running
+ Blink.debug "there are #{running} #{self.object} processes for start"
+ end
+
+ def <=>(other)
+ self.state < 1
+ end
+
+ def fix
+ require 'etc'
+ # ruby is really cool
+ uid = 0
+ if @params[:user].is_a? Integer
+ uid = @params[:user]
+ else
+ uid = Etc.getpwnam(@params[:user]).uid
+ end
+ Kernel.fork {
+ Process.uid = uid
+ Process.euid = uid
+ string = @params[:binary] + (@params[:arguments] || "")
+ Blink::Message.new(
+ :level => :notice,
+ :source => self.object,
+ :message => "starting"
+ )
+ Kernel.exec(string)
+ }
+ end
+ end
+ end
+end
diff --git a/lib/blink/objects/service.rb b/lib/blink/objects/service.rb
new file mode 100755
index 000000000..31fd68234
--- /dev/null
+++ b/lib/blink/objects/service.rb
@@ -0,0 +1,136 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# this is our main way of managing processes right now
+#
+# a service is distinct from a process in that services
+# can only be managed through the interface of an init script
+# which is why they have a search path for initscripts and such
+
+module Blink
+ class Attribute
+ class ServiceRunning < Attribute
+ @name = :running
+
+ def retrieve
+ self.value = self.running()
+ Blink.debug "Running value for '%s' is '%s'" %
+ [self.object.name,self.value]
+ end
+
+ # should i cache this info?
+ def running
+ begin
+ status = self.object.initcmd("status")
+ Blink.debug "initcmd status for '%s' is '%s'" %
+ [self.object.name,status]
+
+ if status # the command succeeded
+ return 1
+ else
+ return 0
+ end
+ rescue SystemCallError
+ raise "Could not execute %s" % initscript
+ end
+
+ end
+
+ def sync
+ if self.running > 0
+ status = 1
+ else
+ status = 0
+ end
+ Blink.debug "'%s' status is '%s' and should be '%s'" %
+ [self,status,should]
+ if self.should > 0
+ if status < 1
+ Blink.debug "Starting '%s'" % self
+ unless self.object.initcmd("start")
+ raise "Failed to start %s" % self.name
+ end
+ else
+ Blink.debug "'%s' is already running, yo" % self
+ #Blink.debug "Starting '%s'" % self
+ #unless self.object.initcmd("start")
+ # raise "Failed to start %s" % self.name
+ #end
+ end
+ elsif status > 0
+ Blink.debug "Stopping '%s'" % self
+ unless self.object.initcmd("stop")
+ raise "Failed to stop %s" % self.name
+ end
+ else
+ Blink.debug "Not running '%s' and shouldn't be running" % self
+ end
+ end
+ end
+ end
+ class Objects
+ class Service < Objects
+ attr_reader :stat
+ @params = [
+ Blink::Attribute::ServiceRunning,
+ :name,
+ :pattern
+ ]
+
+ @objects = Hash.new
+ @actions = Hash.new
+ @namevar = :name
+
+ @searchpaths = Array.new
+
+ def Service.addpath(path)
+ unless @searchpaths.include?(path)
+ # XXX should we check to see if the path exists?
+ @searchpaths.push(path)
+ end
+ end
+
+ def Service.search(name)
+ @searchpaths.each { |path|
+ # must specify that we want the top-level File, not Blink::...::File
+ fqname = ::File.join(path,name)
+ begin
+ stat = ::File.stat(fqname)
+ rescue
+ # should probably rescue specific errors...
+ Blink.debug("Could not find %s in %s" % [name,path])
+ next
+ end
+
+ # if we've gotten this far, we found a valid script
+ return fqname
+ }
+ end
+
+ # it'd be nice if i didn't throw the output away...
+ def initcmd(cmd)
+ script = self.initscript
+
+ Blink.debug "Running '%s %s' as initcmd for '%s'" %
+ [script,cmd,self]
+
+ rvalue = Kernel.system("%s status" %
+ [self.initscript])
+
+ rvalue = Kernel.system("%s %s" %
+ [self.initscript,cmd])
+
+ rvalue
+ end
+
+ def initscript
+ if defined? @initscript
+ return @initscript
+ else
+ @initscript = Service.search(self.name)
+ end
+ end
+ end # Blink::Objects::BProcess
+ end # Blink::Objects
+end
diff --git a/lib/blink/objects/symlink.rb b/lib/blink/objects/symlink.rb
new file mode 100644
index 000000000..11187e2cf
--- /dev/null
+++ b/lib/blink/objects/symlink.rb
@@ -0,0 +1,107 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'etc'
+require 'blink/attribute'
+require 'blink/objects/file'
+
+module Blink
+ # okay, how do we deal with parameters that don't have operations
+ # associated with them?
+ class Attribute
+ class SymlinkTarget < Blink::Attribute
+ require 'etc'
+ attr_accessor :file
+
+ @name = :target
+
+ def create
+ begin
+ Blink.debug("Creating symlink '%s' to '%s'" %
+ [self.object[:path],self.should])
+ unless File.symlink(self.should,self.object[:path])
+ raise TypeError.new("Could not create symlink '%s'" %
+ self.object[:path])
+ end
+ rescue => detail
+ raise TypeError.new("Cannot create symlink '%s': %s" %
+ [self.object[:path],detail])
+ end
+ end
+
+ def remove
+ if FileTest.symlink?(self.object[:path])
+ Blink.debug("Removing symlink '%s'" % self.object[:path])
+ begin
+ File.unlink(self.object[:path])
+ rescue
+ raise TypeError.new("Failed to remove symlink '%s'" %
+ self.object[:path])
+ end
+ elsif FileTest.exists?(self.object[:path])
+ raise TypeError.new("Cannot remove normal file '%s'" %
+ self.object[:path])
+ else
+ Blink.debug("Symlink '%s' does not exist" %
+ self.object[:path])
+ end
+ end
+
+ def retrieve
+ stat = nil
+
+ if FileTest.symlink?(self.object[:path])
+ self.value = File.readlink(self.object[:path])
+ Blink.debug("link value is '%s'" % self.value)
+ return
+ else
+ self.value = nil
+ return
+ end
+ end
+
+ # this is somewhat complicated, because it could exist and be
+ # a file
+ def sync
+ if self.should.nil?
+ self.remove()
+ else # it should exist and be a symlink
+ if FileTest.symlink?(self.object[:path])
+ path = File.readlink(self.object[:path])
+ if path != self.should
+ self.remove()
+ self.create()
+ end
+ elsif FileTest.exists?(self.object[:path])
+ raise TypeError.new("Cannot replace normal file '%s'" %
+ self.object[:path])
+ else
+ self.create()
+ end
+ end
+
+ self.object.newevent(:event => :inode_changed)
+ end
+ end
+ end
+
+ class Objects
+ class Symlink < Objects
+ attr_reader :stat, :path, :params
+ # class instance variable
+ @params = [
+ Blink::Attribute::FileUID,
+ Blink::Attribute::FileGroup,
+ Blink::Attribute::FileMode,
+ Blink::Attribute::SymlinkTarget,
+ :path
+ ]
+
+ @objects = Hash.new
+ @actions = Hash.new
+ @namevar = :path
+ end # Blink::Objects::File
+ end # Blink::Objects
+
+end
diff --git a/lib/blink/oparse.rb b/lib/blink/oparse.rb
new file mode 100644
index 000000000..a96cf1800
--- /dev/null
+++ b/lib/blink/oparse.rb
@@ -0,0 +1,336 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# parse and write configuration files using objects with minimal parsing abilities
+
+require 'etc'
+require 'blink/interface'
+
+module Blink
+ class OParse < Blink::Interface
+ include Enumerable
+
+ attr_accessor :file, :splitchar, :childtype
+
+ @@classes = Hash.new(nil)
+
+ def OParse.[](name)
+ return @@classes[name]
+ end
+
+ def OParse.childtype=(childtype)
+ @childtype = childtype
+ end
+
+ def OParse.childtype
+ return @childtype
+ end
+
+ def OParse.name=(name)
+ @name = name
+ end
+
+ def OParse.regex
+ return @regex
+ end
+
+ def OParse.splitchar=(splitchar)
+ @regex = %r{#{splitchar}}
+ @splitchar = splitchar
+ end
+
+ def OParse.splitchar
+ return @splitchar
+ end
+
+ def OParse.newtype(arghash)
+ options = [:name, :linesplit, :recordsplit, :fields, :namevar]
+
+ #arghash = Hash[*args]
+
+ unless arghash.include?(:linesplit)
+ arghash[:linesplit] = "\n"
+ end
+
+ arghash.each { |key,value|
+ unless options.include?(key)
+ raise "Invalid argument %s on class %s" %
+ [key,self]
+ end
+ }
+ options.each { |option|
+ unless arghash.include?(option)
+ raise "Must pass %s to class %s" %
+ [option,self.class.to_s]
+ end
+ }
+
+ if @@classes.include?(arghash[:name])
+ raise "File type %s already exists" % arghash[:name]
+ end
+
+ klassname = arghash[:name].capitalize
+
+ # create the file type
+ module_eval "
+ class %s < OParse
+ end" % klassname
+ klass = eval(klassname)
+
+ # now create the record type
+ klass.childtype = Blink::OLine.newtype(
+ :name => arghash[:name],
+ :splitchar => arghash[:recordsplit],
+ :fields => arghash[:fields],
+ :namevar => arghash[:namevar]
+ )
+ klass.splitchar = arghash[:linesplit]
+ klass.name = arghash[:name]
+
+ Blink.debug("adding class %s" % arghash[:name])
+ @@classes[arghash[:name]] = klass
+
+ return klass
+ end
+
+ def [](name)
+ return @childhash[name]
+ end
+
+ def []=(name,value)
+ end
+
+ # we don't really have a 'less-than/greater-than' sense here
+ # so i'm sticking with 'equals' until those make sense
+ def ==(other)
+ unless self.children.length == other.children.length
+ Blink.debug("file has %s records instead of %s" %
+ [self.children.length, other.children.length])
+ return self.children.length == other.children.length
+ end
+ equal = true
+ self.zip(other.children) { |schild,ochild|
+ unless schild == ochild
+ Blink.debug("%s has changed in %s" %
+ [schild.name,self.name])
+ equal = false
+ break
+ end
+ }
+
+ return equal
+ end
+
+ def add(&block)
+ obj = self.class.childtype.new(&block)
+ Blink.debug("adding %s" % obj.name)
+ @childary.push(obj)
+ @childhash[obj.name] = obj
+
+ return obj
+ end
+
+ def children
+ return @childary
+ end
+
+ def delete(name)
+ if @childhash.has_key?(name)
+ child = @childhash[name]
+
+ @childhash.delete(child)
+ @childary.delete(child)
+ else
+ raise "No such entry %s" % name
+ end
+ end
+
+ def each
+ @childary.each { |child|
+ yield child
+ }
+ end
+
+ def initialize(file)
+ @file = file
+
+ @childary = []
+ @childhash = {}
+ end
+
+ # this is where we're pretty different from other objects
+ # we can choose to either reparse the existing file and compare
+ # the objects, or we can write our file out and do an
+ # text comparison
+ def insync?
+ tmp = self.class.new(@file)
+ tmp.retrieve
+
+ return self == tmp
+ end
+
+ def retrieve
+ str = ""
+ File.open(@file) { |fname|
+ fname.each { |line|
+ str += line
+ }
+ }
+
+ @childary = str.split(self.class.regex).collect { |record|
+ child = self.class.childtype.new
+ child.record = record
+ #puts "adding child %s" % child.name
+ child
+ }
+
+ @childary.each { |child|
+ @childhash[child.name] = child
+ }
+ end
+
+ def sync
+ #unless self.insync?
+ self.write
+ #end
+ end
+
+ def to_s
+ return @childary.collect { |child|
+ child.to_s
+ }.join(self.class.splitchar) + self.class.splitchar
+ end
+
+ def write
+ File.open(@file, "w") { |file|
+ file.write(self.to_s)
+ }
+ end
+ end
+
+ class OLine < Blink::Interface
+ attr_accessor :fields, :namevar, :splitchar
+
+ @@subclasses = {}
+
+ def OLine.fields=(ary)
+ @fields = ary
+ end
+
+ def OLine.fields
+ return @fields
+ end
+
+ #def OLine.newtype(name,splitchar,fields,namevar)
+ def OLine.newtype(*args)
+ options = [:name, :splitchar, :fields, :namevar]
+
+ arghash = Hash[*args]
+ arghash.each { |key,value|
+ unless options.include?(key)
+ raise "Invalid argument %s on class %s" %
+ [key,self.class.to_s]
+ end
+ }
+ options.each { |option|
+ unless arghash.include?(option)
+ raise "Must pass %s to class %s" %
+ [option,self.class.to_s]
+ end
+ }
+ klassname = arghash[:name].capitalize
+
+ module_eval "
+ class %s < OLine
+ end" % klassname
+ klass = eval(klassname)
+
+ klass.fields = arghash[:fields]
+ klass.splitchar = arghash[:splitchar]
+ klass.namevar = arghash[:namevar]
+
+ return klass
+ end
+
+ def OLine.namevar=(field)
+ @namevar = field
+ end
+
+ def OLine.namevar
+ return @namevar
+ end
+
+ def OLine.regex
+ return @regex
+ end
+
+ def OLine.splitchar=(char)
+ @splitchar = char
+ @regex = %r{#{char}}
+ end
+
+ def OLine.splitchar
+ return @splitchar
+ end
+
+ def [](field)
+ @fields[field]
+ end
+
+ def []=(field,value)
+ @fields[field] = value
+ end
+
+ def ==(other)
+ unless self.class == other.class
+ return false
+ end
+
+ unless self.name == other.name
+ return false
+ end
+ @fields.keys { |field|
+ unless self[field] == other[field]
+ Blink.debug("%s -> %s has changed" % [self.name, field])
+ return false
+ end
+ }
+ return true
+ end
+
+
+ def initialize
+ @fields = {}
+ if block_given?
+ yield self
+ end
+ end
+
+ def record=(record)
+ ary = record.split(self.class.regex)
+ self.class.fields.each { |field|
+ @fields[field] = ary.shift
+ #puts "%s => %s" % [field,@fields[field]]
+ }
+ end
+
+ def name
+ if @fields.include?(self.class.namevar)
+ return @fields[self.class.namevar]
+ else
+ raise "No namevar for objects of type %s" % self.class.to_s
+ end
+ end
+
+ def to_s
+ ary = self.class.fields.collect { |field|
+ if ! @fields.include?(field)
+ raise "Object %s is missing field %s" % [self.name,field]
+ else
+ @fields[field]
+ end
+ }.join(self.class.splitchar)
+ end
+ end
+end
diff --git a/lib/blink/parser/grammar.ra b/lib/blink/parser/grammar.ra
new file mode 100644
index 000000000..ab429a662
--- /dev/null
+++ b/lib/blink/parser/grammar.ra
@@ -0,0 +1,451 @@
+#/usr/bin/ruby
+
+# $Id$
+# vim: syntax=ruby
+
+# the parser
+
+class Blink::Parser::Parser
+token WORD LBRACK QTEXT RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE FALSE EQUALS
+token QMARK LPAREN RPAREN
+rule
+program: statements {
+ if val[0].is_a?(Array)
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0]])
+ end
+ # this is mainly so we can test the parser separately from the
+ # interpreter
+ if Blink[:parseonly]
+ begin
+ puts result.tree(0)
+ rescue NoMethodError => detail
+ puts detail
+ exit(78)
+ end
+ else
+ require 'blink/parser/interpreter'
+ result = Blink::Parser::Interpreter.new(result)
+ end
+}
+
+statements: statement
+ | statements statement {
+ if val[0].is_a?(Array)
+ val[0].push(val[1])
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0],val[1]])
+ end
+}
+
+statement: object
+ | assignment
+ | selector
+ | functioncall
+
+object: WORD LBRACK rvalue RBRACK LBRACE params endcomma RBRACE {
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::ObjectDef.new(
+ :pin => "[]",
+ :line => @lexer.line,
+ :object => leaf,
+ :name => val[2],
+ :params => val[5]
+ )
+}
+
+assignment: WORD EQUALS rvalue {
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::VarDef.new(
+ :pin => "=",
+ :line => @lexer.line,
+ :name => leaf,
+ :value => val[2]
+ )
+}
+
+params: param { result = val[0] }
+ | params COMMA param {
+ if val[0].is_a?(Array)
+ val[0].push(val[2])
+ result = val[0]
+ else
+ result = [val[0],val[2]]
+ end
+}
+
+param: QTEXT FARROW rvalue {
+ leaf = AST::String.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::ObjectParam.new(
+ :pin => "=>",
+ :line => @lexer.line,
+ :param => leaf,
+ :value => val[2]
+ )
+}
+
+rvalues: rvalue
+ | rvalues rvalue {
+ if val[0].is_a?(Array)
+ result = val[0].push(val[1])
+ else
+ result = AST::Array.new(val[0],val[1])
+ end
+}
+
+rvalue: QTEXT {
+ result = AST::String.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+}
+ | selector
+ | object
+ | functioncall
+ | WORD { # these are variable names
+ result = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+}
+
+selector: WORD QMARK svalues {
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::Selector.new(
+ :pin => "?",
+ :line => @lexer.line,
+ :param => leaf,
+ :value => val[2]
+ )
+}
+
+# I'm probably going to regret reusing 'param' here...
+svalues: param
+ | LBRACE sintvalues RBRACE { result = val[1] }
+
+sintvalues: param
+ | sintvalues param {
+ if val[0].is_a?(Array)
+ val[0].push(val[1])
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0],val[1]])
+ end
+}
+
+functioncall: WORD LPAREN rvalues RPAREN {
+ result = AST::FunctionCall.new(
+ :pin => '()',
+ :name => AST::Word.new(:value => val[0], :line => @lexer.line),
+ :values => val[2]
+ )
+}
+ | WORD LPAREN RPAREN {
+ result = FunctionDef.new(
+ :pin => '()',
+ :name => val[0]
+ )
+}
+
+endcomma: # nothing
+ | COMMA { result = nil }
+
+end
+---- header ----
+require 'blink/parser/lexer'
+#require 'blink/parser/interpreter'
+
+module Blink
+ class ParseError < Racc::ParseError; end
+end
+
+---- inner ----
+def file=(file)
+ @lexer.file = file
+end
+
+def initialize
+ @lexer = Blink::Parser::Lexer.new()
+ if Blink[:debug]
+ @yydebut = true
+ end
+end
+
+def on_error(token,value,stack)
+ #puts "Parse stack:"
+ #puts stack
+ #on '%s' at '%s' in\n'%s'" % [token,value,stack]
+ error = "line %s: parse error after '%s'" % [@lexer.line,@lexer.last]
+
+ if @lexer.file
+ error += (" in '%s'" % @lexer.file)
+ end
+
+ raise Blink::ParseError.new(error)
+end
+
+# how should I do error handling here?
+def parse
+ yyparse(@lexer,:scan)
+ #begin
+ # yyparse(@lexer,:scan)
+ #rescue Racc::ParseError => detail
+ # raise Racc::ParseError.new("line %s: parse error after '%s'" %
+ # [@lexer.line,@lexer.last])
+ #end
+end
+
+def string=(string)
+ @lexer.string = string
+end
+
+# the parent class for all of our syntactical objects
+class AST
+ attr_accessor :line
+ @@pink = ""
+ @@green = ""
+ @@yellow = ""
+ @@reset = ""
+
+ @@indent = " " * 4
+ @@indline = @@pink + ("-" * 4) + @@reset
+ @@midline = @@yellow + ("-" * 4) + @@reset
+
+ def AST.indention
+ return @@indent * @@indention
+ end
+
+ def AST.midline
+ return @@midline
+ end
+
+ def typewrap(string)
+ #return self.class.to_s.sub(/.+::/,'') + "(" + @@green + string +@@reset+ ")"
+ return @@green + string +@@reset+ "(" + self.class.to_s.sub(/.+::/,'') + ")"
+ end
+
+ def initialize(*rest)
+ begin
+ args = Hash[*rest]
+ rescue ArgumentError
+ raise ArgumentError.new("Arguments must be passed as name => value pairs")
+ end
+ args.each { |param,value|
+ method = param.to_s + "="
+ unless self.respond_to?(method)
+ raise "Invalid parameter %s to object class %s" %
+ [method,self.class.to_s]
+ end
+
+ begin
+ #Blink.debug("sending %s to %s" % [method, self.class])
+ self.send(method,value)
+ rescue => detail
+ # XXX this should be more normal error correction
+ raise "Could not set parameter %s on class %s: %s" %
+ [method,self.class.to_s,detail]
+ end
+ }
+ end
+
+ class ASTArray < Array
+ def tree(indent = 0)
+ #puts((AST.indent * indent) + self.pin)
+ self.collect { |child|
+ child.tree(indent)
+ }.join("\n" + (AST.midline * (indent+1)) + "\n")
+ end
+ end
+
+ # this differentiation is used by the interpreter
+ # XXX i now need a standard mechanism for descending into children
+
+ # these objects have children
+ class Branch < AST
+ include Enumerable
+ attr_accessor :pin
+
+ def each
+ @children.each { |child|
+ yield child
+ }
+ end
+
+ def tree(indent = 0)
+ return ((@@indline * indent) + self.typewrap(self.pin)) + "\n" +
+ self.collect { |child|
+ child.tree(indent + 1)
+ }.join("\n")
+ end
+ end
+
+ # and these ones don't
+ class Leaf < AST
+ attr_accessor :value, :type
+
+ def tree(indent = 0)
+ return ((@@indent * indent) + self.typewrap(self.value))
+ end
+
+ def to_s
+ return @value
+ end
+ end
+
+ class String < AST::Leaf
+ attr_accessor :value
+ end
+
+ class Word < AST::Leaf
+ attr_accessor :value
+ end
+
+ class ObjectDef < AST::Branch
+ attr_accessor :name, :object
+ attr_reader :params
+
+ def []=(index,obj)
+ @params[index] = obj
+ end
+
+ def [](index)
+ return @params[index]
+ end
+
+ def each
+ #Blink.debug("each called on %s" % self)
+ [@object,@name,@params].flatten.each { |param|
+ #Blink.debug("yielding param %s" % param)
+ yield param
+ }
+ end
+
+ def initialize(*args)
+ super(*args)
+ end
+
+ def params=(params)
+ if params.is_a?(Array)
+ @params = params
+ else
+ @params = [params]
+ end
+ end
+
+ def tree(indent = 0)
+ return [
+ @object.tree(indent + 1),
+ @name.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @params.collect { |param|
+ begin
+ param.tree(indent + 1)
+ rescue NoMethodError => detail
+ puts "failed to tree"
+ puts @params
+ p param
+ raise
+ end
+ }.join("\n")
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => { %s }" % [@name,
+ @params.collect { |param|
+ param.to_s
+ }.join("\n")
+ ]
+ end
+ end
+
+ class ObjectParam < AST::Branch
+ attr_accessor :value, :param
+
+ def each
+ [@param,@value].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @param.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@param,@value]
+ end
+ end
+
+ class Selector < AST::Branch
+ attr_accessor :param, :value
+
+ def tree(indent = 0)
+ return [
+ @param.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def each
+ [@param,@value].each { |child| yield child }
+ end
+ end
+
+ class VarDef < AST::Branch
+ attr_accessor :name, :value
+
+ def each
+ [@name,@value].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @name.tree(indent + 1),
+ ((@@indline * 4 * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@name,@value]
+ end
+ end
+
+ class FunctionCall < AST::Branch
+ attr_accessor :name, :values
+
+ def each
+ [@name,@values].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @name.tree(indent + 1),
+ ((@@indline * 4 * indent) + self.typewrap(self.pin)),
+ @values.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@name,@values]
+ end
+ end
+end
diff --git a/lib/blink/parser/interpreter.rb b/lib/blink/parser/interpreter.rb
new file mode 100644
index 000000000..0e117fb61
--- /dev/null
+++ b/lib/blink/parser/interpreter.rb
@@ -0,0 +1,222 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the interpreter
+#
+# this builds our virtual pinball machine, into which we'll place our host-specific
+# information and out of which we'll receive our host-specific configuration
+
+require 'strscan'
+require 'blink'
+require 'blink/parser/parser'
+
+
+module Blink
+ class IntepreterError < RuntimeError; end
+ module Parser
+ #---------------------------------------------------------------
+ class Interpreter
+ # just shorten the constant path a bit, using what amounts to an alias
+ AST = Blink::Parser::Parser::AST
+
+ # make it a class method, since it's not an instance method...
+ def Interpreter.descend(root,depthfirst = true,&block)
+ #Blink.debug("root is %s of type %s" % [root,root.class])
+ root.each_with_index { |thing,index|
+ # this is a problem...
+ # we want to descend into all syntactical objects, but
+ # we don't want to descend into Blink::Objects because
+ # that would mean operating directly on attributes, which
+ # we don't want
+ if depthfirst
+ if thing.is_a?(AST::Branch)
+ Blink.debug("descending thing %s of type %s" %
+ [thing,thing.class])
+ Interpreter.descend(thing,&block)
+ end
+ block.call(thing,index,root)
+ else
+ block.call(thing,index,root)
+ if thing.is_a?(AST::Branch)
+ Blink.debug("descending thing %s of type %s" %
+ [thing,thing.class])
+ Interpreter.descend(thing,&block)
+ end
+ end
+ }
+ end
+
+ #------------------------------------------------------------
+ def askfunc(name,*args)
+ if func = Blink::Function[name]
+ # XXX when we're remote, we'll need to do this differently...
+ func.call(*args)
+ else
+ raise "Undefined function %s" % name
+ end
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # when we have an 'eval' function, we should do that instead
+ # for now, we only support variables in strings
+ def strinterp(string)
+ regex = Regexp.new('\$\{(\w+)\}}')
+ while match = regex.match(string) do
+ string.sub!(regex,self.varvalue(match[0]))
+ end
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # basically just return the variable value from the symbol
+ # table
+ def varvalue(variable)
+ unless @symtable.include?(variable)
+ raise "Undefined variable %s" % variable
+ end
+
+ return @symtable[variable]
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # create our interpreter
+ def initialize(tree)
+ @tree = tree
+
+ @symtable = Hash.new(nil)
+ @factable = Hash.new(nil)
+ @objectable = Hash.new { |hash,key|
+ #hash[key] = IObject.new(key)
+ hash[key] = {:name => key}
+ }
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # execute all of the passes (probably just one, in the end)
+ def run
+ regex = %r{^pass}
+ self.methods.sort.each { |method,value|
+ if method =~ regex
+ Blink.debug("calling %s" % method)
+ self.send(method)
+ end
+ }
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # i don't know how to deal with evaluation here --
+ # all Leafs need to be turned into real values, but only in
+ # those trees which i know are under 'true' branches
+ def pass1_umeverything
+ Interpreter.descend(@tree) { |object,index,parent|
+ case object
+ # handle the leaves first
+ when AST::String then
+ # interpolate all variables in the string in-place
+ self.strinterp(object.value)
+ when AST::Word then
+ if parent.is_a?(AST::VarDef) # if we're in an assignment
+ # um, we pretty much don't do anything
+ else
+ # this is where i interpolate the variable, right?
+ # replace the variable AST with a string AST, I guess
+ # unless, of course, the variable points to another
+ # object...
+ # crap, what if it does?
+ end
+ when AST::VarDef then
+ unless object.name.is_a?(AST::Word)
+ raise InterpreterError.new("invalid variable name")
+ end
+
+ # this is quite probably more than a simple value...
+ case object.value
+ when AST::String then
+ @symtable[object.name.value] = object.value.value
+ when AST::Word then
+ # just copy whatever's already in the symtable
+ @symtable[object.name.value] =
+ @symtable[object.value.value]
+ else
+ # um, i have no idea what to do in other cases...
+ end
+ when AST::FunctionCall then
+ when AST::ObjectDef then
+ object.params.each { |param|
+ }
+ end
+ }
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # this pass creates the actual objects
+ # eventually it will probably be one of the last passes, but
+ # it's the easiest to create, so...
+
+ # XXX this won't really work for the long term --
+ # this will cause each operation on an object to be treated
+ # as an independent copy of the object, which will fail
+ # purposefully
+ def disabled_pass1_mkobjects
+ Interpreter.descend(@tree) { |object,index,parent|
+ case object
+ when Blink::Parser::Parser::AST::ObjectDef then # yuk
+ args = {}
+ object.each { |param|
+ # heh, this is weird
+ # the parameter object stores its value in @value
+ # and that's an object, so you have to call .value
+ # again
+ args[param.param] = param.value.value
+ }
+
+ args[:name] = object.name.value
+ klass = "Blink::Objects::" + object.type.capitalize
+ newobj = eval(klass).new(args)
+ parent[index] = newobj
+ when Blink::Parser::Parser::AST::ObjectParam then
+ # nothing
+ end
+ }
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ def disabled_pass2_exeobjects
+ Blink.debug("tree is %s" % @tree)
+ Blink.debug("tree type is %s" % @tree.class)
+ Interpreter.descend(@tree) { |object,index,parent|
+ #Blink.debug("object is %s" % object)
+ puts("object is %s" % object)
+ case
+ when object.is_a?(Blink::Objects) then
+ object.evaluate
+ end
+ }
+ end
+
+ class IObject < Hash
+ attr_accessor :name
+
+ @ohash = {}
+ @oarray = []
+
+ def initialize(name)
+ if @ohash.include?(name)
+ raise "%s already exists" % name
+ else
+ @ohash[name] = self
+ @oarray.push(self)
+ end
+ end
+ end
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/blink/parser/lexer.rb b/lib/blink/parser/lexer.rb
new file mode 100644
index 000000000..66e75a315
--- /dev/null
+++ b/lib/blink/parser/lexer.rb
@@ -0,0 +1,182 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the scanner/lexer
+
+require 'strscan'
+require 'blink'
+
+
+module Blink
+ class LexError < RuntimeError; end
+ module Parser
+ #---------------------------------------------------------------
+ class Lexer
+ attr_reader :line, :last, :file
+
+ @@tokens = {
+ %r{#.+} => :COMMENT,
+ %r{\[} => :LBRACK,
+ %r{\]} => :RBRACK,
+ %r{\{} => :LBRACE,
+ %r{\}} => :RBRACE,
+ %r{\(} => :LPAREN,
+ %r{\)} => :RPAREN,
+ %r{"} => :DQUOTE,
+ %r{\n} => :RETURN,
+ %r{'} => :SQUOTE,
+ %r{=} => :EQUALS,
+ %r{,} => :COMMA,
+ %r{\?} => :QMARK,
+ %r{\\} => :BACKSLASH,
+ %r{=>} => :FARROW,
+ %r{\w+} => :WORD,
+ %r{:\w+} => :SYMBOL
+ }
+
+ # scan the whole file
+ # basically just used for testing
+ def fullscan
+ array = []
+
+ self.scan { |token,str|
+ #Blink.debug("got token '%s' => '%s'" % [token,str])
+ if token.nil?
+ return array
+ else
+ array.push([token,str])
+ end
+ }
+ return array
+ end
+
+ # this is probably pretty damned inefficient...
+ # it'd be nice not to have to load the whole file first...
+ def file=(file)
+ @file = file
+ File.open(file) { |of|
+ str = ""
+ of.each { |line| str += line }
+ @scanner = StringScanner.new(str)
+ }
+ end
+
+ def initialize
+ @line = 1
+ @last = ""
+ @scanner = nil
+ @file = nil
+ @skip = %r{\s+}
+ end
+
+ def rest
+ @scanner.rest
+ end
+
+ # this is the heart of the lexer
+ def scan
+ Blink.debug("entering scan")
+ if @scanner.nil?
+ raise TypeError.new("Invalid or empty string")
+ end
+
+ @scanner.skip(@skip)
+ until @scanner.eos? do
+ yielded = false
+ sendbreak = false # gah, this is a nasty hack
+ stoken = nil
+ sregex = nil
+ value = ""
+
+ # first find out which type of token we've got
+ @@tokens.each { |regex,token|
+ # we're just checking, which doesn't advance the scan
+ # pointer
+ tmp = @scanner.check(regex)
+ if tmp.nil?
+ #blink.debug("did not match %s to '%s'" %
+ # [regex,@scanner.rest])
+ next
+ end
+
+ # find the longest match
+ if tmp.length > value.length
+ value = tmp
+ stoken = token
+ sregex = regex
+ else
+ # we've already got a longer match
+ next
+ end
+ }
+
+ # error out if we didn't match anything at all
+ if stoken.nil?
+ raise "Could not match '%s'" % @scanner.rest
+ end
+
+ value = @scanner.scan(sregex)
+
+ if value == ""
+ raise "Didn't match regex on token %s" % stoken
+ end
+
+ # token-specific operations
+ # if this gets much more complicated, it should
+ # be moved up to where the tokens themselves are defined
+ # which will get me about 75% of the way to a lexer generator
+ case stoken
+ when :COMMENT then
+ # just throw comments away
+ when :RETURN then
+ Blink.debug("one more line")
+ @line += 1
+ @scanner.skip(@skip)
+ when :DQUOTE then
+ #Blink.debug("searching '%s' after '%s'" % [self.rest,value])
+ value = self.slurpstring(value)
+ yield [:QTEXT,value]
+ @last = value
+ #stoken = :QTEXT
+ Blink.debug("got string '%s' => '%s'" % [:QTEXT,value])
+ when :SYMBOL then
+ value.sub!(/^:/,'')
+ yield [:QTEXT,value]
+ @last = value
+ Blink.debug("got token '%s' => '%s'" % [:QTEXT,value])
+ else
+ yield [stoken,value]
+ @last = value
+ Blink.debug("got token '%s' => '%s'" % [stoken,value])
+ end
+ @scanner.skip(@skip)
+ end
+ @scanner = nil
+ yield [false,false]
+ end
+
+ # we've encountered an opening quote...
+ # slurp in the rest of the string and return it
+ def slurpstring(quote)
+ #Blink.debug("searching '%s'" % self.rest)
+ str = @scanner.scan_until(/[^\\]#{quote}/)
+ #str = @scanner.scan_until(/"/)
+ if str.nil?
+ raise Blink::LexError.new("Unclosed quote after '%s' in '%s'" %
+ [self.last,self.rest])
+ else
+ str.sub!(/#{quote}$/,"")
+ str.gsub!(/\\#{quote}/,quote)
+ end
+
+ return str
+ end
+
+ def string=(string)
+ @scanner = StringScanner.new(string)
+ end
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/blink/parser/makefile b/lib/blink/parser/makefile
new file mode 100644
index 000000000..eea119d56
--- /dev/null
+++ b/lib/blink/parser/makefile
@@ -0,0 +1,5 @@
+#parser.rb: grammar.ry
+# ryacc --output parser grammar
+
+parser.rb: grammar.ra
+ racc -o$@ grammar.ra
diff --git a/lib/blink/parser/parser.rb b/lib/blink/parser/parser.rb
new file mode 100644
index 000000000..2c341f410
--- /dev/null
+++ b/lib/blink/parser/parser.rb
@@ -0,0 +1,683 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by racc 1.4.4
+# from racc grammer file "grammar.ra".
+#
+
+require 'racc/parser'
+
+
+require 'blink/parser/lexer'
+#require 'blink/parser/interpreter'
+
+module Blink
+ class ParseError < Racc::ParseError; end
+end
+
+
+module Blink
+
+ module Parser
+
+ class Parser < Racc::Parser
+
+module_eval <<'..end grammar.ra modeval..id5273b1fd0f', 'grammar.ra', 171
+def file=(file)
+ @lexer.file = file
+end
+
+def initialize
+ @lexer = Blink::Parser::Lexer.new()
+ if Blink[:debug]
+ @yydebut = true
+ end
+end
+
+def on_error(token,value,stack)
+ #puts "Parse stack:"
+ #puts stack
+ #on '%s' at '%s' in\n'%s'" % [token,value,stack]
+ error = "line %s: parse error after '%s'" % [@lexer.line,@lexer.last]
+
+ if @lexer.file
+ error += (" in '%s'" % @lexer.file)
+ end
+
+ raise Blink::ParseError.new(error)
+end
+
+# how should I do error handling here?
+def parse
+ yyparse(@lexer,:scan)
+ #begin
+ # yyparse(@lexer,:scan)
+ #rescue Racc::ParseError => detail
+ # raise Racc::ParseError.new("line %s: parse error after '%s'" %
+ # [@lexer.line,@lexer.last])
+ #end
+end
+
+def string=(string)
+ @lexer.string = string
+end
+
+# the parent class for all of our syntactical objects
+class AST
+ attr_accessor :line
+ @@pink = ""
+ @@green = ""
+ @@yellow = ""
+ @@reset = ""
+
+ @@indent = " " * 4
+ @@indline = @@pink + ("-" * 4) + @@reset
+ @@midline = @@yellow + ("-" * 4) + @@reset
+
+ def AST.indention
+ return @@indent * @@indention
+ end
+
+ def AST.midline
+ return @@midline
+ end
+
+ def typewrap(string)
+ #return self.class.to_s.sub(/.+::/,'') + "(" + @@green + string +@@reset+ ")"
+ return @@green + string +@@reset+ "(" + self.class.to_s.sub(/.+::/,'') + ")"
+ end
+
+ def initialize(*rest)
+ begin
+ args = Hash[*rest]
+ rescue ArgumentError
+ raise ArgumentError.new("Arguments must be passed as name => value pairs")
+ end
+ args.each { |param,value|
+ method = param.to_s + "="
+ unless self.respond_to?(method)
+ raise "Invalid parameter %s to object class %s" %
+ [method,self.class.to_s]
+ end
+
+ begin
+ #Blink.debug("sending %s to %s" % [method, self.class])
+ self.send(method,value)
+ rescue => detail
+ # XXX this should be more normal error correction
+ raise "Could not set parameter %s on class %s: %s" %
+ [method,self.class.to_s,detail]
+ end
+ }
+ end
+
+ class ASTArray < Array
+ def tree(indent = 0)
+ #puts((AST.indent * indent) + self.pin)
+ self.collect { |child|
+ child.tree(indent)
+ }.join("\n" + (AST.midline * (indent+1)) + "\n")
+ end
+ end
+
+ # this differentiation is used by the interpreter
+ # XXX i now need a standard mechanism for descending into children
+
+ # these objects have children
+ class Branch < AST
+ include Enumerable
+ attr_accessor :pin
+
+ def each
+ @children.each { |child|
+ yield child
+ }
+ end
+
+ def tree(indent = 0)
+ return ((@@indline * indent) + self.typewrap(self.pin)) + "\n" +
+ self.collect { |child|
+ child.tree(indent + 1)
+ }.join("\n")
+ end
+ end
+
+ # and these ones don't
+ class Leaf < AST
+ attr_accessor :value, :type
+
+ def tree(indent = 0)
+ return ((@@indent * indent) + self.typewrap(self.value))
+ end
+
+ def to_s
+ return @value
+ end
+ end
+
+ class String < AST::Leaf
+ attr_accessor :value
+ end
+
+ class Word < AST::Leaf
+ attr_accessor :value
+ end
+
+ class ObjectDef < AST::Branch
+ attr_accessor :name, :object
+ attr_reader :params
+
+ def []=(index,obj)
+ @params[index] = obj
+ end
+
+ def [](index)
+ return @params[index]
+ end
+
+ def each
+ #Blink.debug("each called on %s" % self)
+ [@object,@name,@params].flatten.each { |param|
+ #Blink.debug("yielding param %s" % param)
+ yield param
+ }
+ end
+
+ def initialize(*args)
+ super(*args)
+ end
+
+ def params=(params)
+ if params.is_a?(Array)
+ @params = params
+ else
+ @params = [params]
+ end
+ end
+
+ def tree(indent = 0)
+ return [
+ @object.tree(indent + 1),
+ @name.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @params.collect { |param|
+ begin
+ param.tree(indent + 1)
+ rescue NoMethodError => detail
+ puts "failed to tree"
+ puts @params
+ p param
+ raise
+ end
+ }.join("\n")
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => { %s }" % [@name,
+ @params.collect { |param|
+ param.to_s
+ }.join("\n")
+ ]
+ end
+ end
+
+ class ObjectParam < AST::Branch
+ attr_accessor :value, :param
+
+ def each
+ [@param,@value].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @param.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@param,@value]
+ end
+ end
+
+ class Selector < AST::Branch
+ attr_accessor :param, :value
+
+ def tree(indent = 0)
+ return [
+ @param.tree(indent + 1),
+ ((@@indline * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def each
+ [@param,@value].each { |child| yield child }
+ end
+ end
+
+ class VarDef < AST::Branch
+ attr_accessor :name, :value
+
+ def each
+ [@name,@value].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @name.tree(indent + 1),
+ ((@@indline * 4 * indent) + self.typewrap(self.pin)),
+ @value.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@name,@value]
+ end
+ end
+
+ class FunctionCall < AST::Branch
+ attr_accessor :name, :values
+
+ def each
+ [@name,@values].each { |child| yield child }
+ end
+
+ def tree(indent = 0)
+ return [
+ @name.tree(indent + 1),
+ ((@@indline * 4 * indent) + self.typewrap(self.pin)),
+ @values.tree(indent + 1)
+ ].join("\n")
+ end
+
+ def to_s
+ return "%s => %s" % [@name,@values]
+ end
+ end
+end
+..end grammar.ra modeval..id5273b1fd0f
+
+##### racc 1.4.4 generates ###
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 1, 18, :_reduce_1,
+ 1, 19, :_reduce_none,
+ 2, 19, :_reduce_3,
+ 1, 20, :_reduce_none,
+ 1, 20, :_reduce_none,
+ 1, 20, :_reduce_none,
+ 1, 20, :_reduce_none,
+ 8, 21, :_reduce_8,
+ 3, 22, :_reduce_9,
+ 1, 26, :_reduce_10,
+ 3, 26, :_reduce_11,
+ 3, 28, :_reduce_12,
+ 1, 29, :_reduce_none,
+ 2, 29, :_reduce_14,
+ 1, 25, :_reduce_15,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 3, 23, :_reduce_19,
+ 1, 30, :_reduce_none,
+ 3, 30, :_reduce_21,
+ 1, 31, :_reduce_none,
+ 2, 31, :_reduce_23,
+ 4, 24, :_reduce_24,
+ 3, 24, :_reduce_25,
+ 0, 27, :_reduce_none,
+ 1, 27, :_reduce_27 ]
+
+racc_reduce_n = 28
+
+racc_shift_n = 46
+
+racc_action_table = [
+ 17, 17, 19, 19, 21, 30, 11, 38, 11, 17,
+ 21, 19, 22, 29, 35, 27, 9, 10, 12, 10,
+ 12, 17, 17, 19, 19, 21, 33, 3, 13, 3,
+ 39, 21, 43, 44, 21 ]
+
+racc_action_check = [
+ 28, 12, 28, 12, 32, 21, 3, 32, 17, 11,
+ 10, 11, 10, 13, 28, 12, 3, 3, 3, 17,
+ 17, 30, 9, 30, 9, 22, 25, 6, 5, 0,
+ 33, 39, 40, 42, 43 ]
+
+racc_action_pointer = [
+ 27, nil, nil, 3, nil, 28, 25, nil, nil, 20,
+ 6, 7, -1, 13, nil, nil, nil, 5, nil, nil,
+ nil, -4, 21, nil, nil, 21, nil, nil, -2, nil,
+ 19, nil, 0, 24, nil, nil, nil, nil, nil, 27,
+ 22, nil, 26, 30, nil, nil ]
+
+racc_action_default = [
+ -28, -5, -6, -28, -7, -28, -1, -2, -4, -28,
+ -28, -28, -28, -28, -3, -16, -18, -28, -9, -15,
+ -17, -28, -28, -20, -19, -28, -13, -25, -28, 46,
+ -28, -22, -28, -28, -14, -24, -12, -23, -21, -28,
+ -26, -10, -28, -27, -8, -11 ]
+
+racc_goto_table = [
+ 23, 8, 18, 7, 25, 26, 5, 8, 2, 14,
+ 4, 40, 31, 42, 2, 6, 4, 28, 24, 32,
+ nil, 34, 37, 36, nil, nil, nil, nil, nil, 41,
+ nil, nil, nil, 45 ]
+
+racc_goto_check = [
+ 11, 4, 8, 3, 8, 8, 1, 4, 6, 3,
+ 7, 9, 11, 10, 6, 2, 7, 12, 13, 14,
+ nil, 8, 11, 8, nil, nil, nil, nil, nil, 11,
+ nil, nil, nil, 11 ]
+
+racc_goto_pointer = [
+ nil, 6, 15, 3, 1, nil, 8, 10, -7, -28,
+ -27, -10, 5, 8, -3 ]
+
+racc_goto_default = [
+ nil, nil, nil, nil, 20, 1, 15, 16, nil, nil,
+ nil, nil, nil, nil, nil ]
+
+racc_token_table = {
+ false => 0,
+ Object.new => 1,
+ :WORD => 2,
+ :LBRACK => 3,
+ :QTEXT => 4,
+ :RBRACK => 5,
+ :LBRACE => 6,
+ :RBRACE => 7,
+ :SYMBOL => 8,
+ :FARROW => 9,
+ :COMMA => 10,
+ :TRUE => 11,
+ :FALSE => 12,
+ :EQUALS => 13,
+ :QMARK => 14,
+ :LPAREN => 15,
+ :RPAREN => 16 }
+
+racc_use_result_var = true
+
+racc_nt_base = 17
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+'$end',
+'error',
+'WORD',
+'LBRACK',
+'QTEXT',
+'RBRACK',
+'LBRACE',
+'RBRACE',
+'SYMBOL',
+'FARROW',
+'COMMA',
+'TRUE',
+'FALSE',
+'EQUALS',
+'QMARK',
+'LPAREN',
+'RPAREN',
+'$start',
+'program',
+'statements',
+'statement',
+'object',
+'assignment',
+'selector',
+'functioncall',
+'rvalue',
+'params',
+'endcomma',
+'param',
+'rvalues',
+'svalues',
+'sintvalues']
+
+Racc_debug_parser = false
+
+##### racc system variables end #####
+
+ # reduce 0 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 31
+ def _reduce_1( val, _values, result )
+ if val[0].is_a?(Array)
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0]])
+ end
+ # this is mainly so we can test the parser separately from the
+ # interpreter
+ if Blink[:parseonly]
+ begin
+ puts result.tree(0)
+ rescue NoMethodError => detail
+ puts detail
+ exit(78)
+ end
+ else
+ require 'blink/parser/interpreter'
+ result = Blink::Parser::Interpreter.new(result)
+ end
+ result
+ end
+.,.,
+
+ # reduce 2 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 41
+ def _reduce_3( val, _values, result )
+ if val[0].is_a?(Array)
+ val[0].push(val[1])
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0],val[1]])
+ end
+ result
+ end
+.,.,
+
+ # reduce 4 omitted
+
+ # reduce 5 omitted
+
+ # reduce 6 omitted
+
+ # reduce 7 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 60
+ def _reduce_8( val, _values, result )
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::ObjectDef.new(
+ :pin => "[]",
+ :line => @lexer.line,
+ :object => leaf,
+ :name => val[2],
+ :params => val[5]
+ )
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 73
+ def _reduce_9( val, _values, result )
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::VarDef.new(
+ :pin => "=",
+ :line => @lexer.line,
+ :name => leaf,
+ :value => val[2]
+ )
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 74
+ def _reduce_10( val, _values, result )
+ result = val[0]
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 83
+ def _reduce_11( val, _values, result )
+ if val[0].is_a?(Array)
+ val[0].push(val[2])
+ result = val[0]
+ else
+ result = [val[0],val[2]]
+ end
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 96
+ def _reduce_12( val, _values, result )
+ leaf = AST::String.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::ObjectParam.new(
+ :pin => "=>",
+ :line => @lexer.line,
+ :param => leaf,
+ :value => val[2]
+ )
+ result
+ end
+.,.,
+
+ # reduce 13 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 105
+ def _reduce_14( val, _values, result )
+ if val[0].is_a?(Array)
+ result = val[0].push(val[1])
+ else
+ result = AST::Array.new(val[0],val[1])
+ end
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 112
+ def _reduce_15( val, _values, result )
+ result = AST::String.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result
+ end
+.,.,
+
+ # reduce 16 omitted
+
+ # reduce 17 omitted
+
+ # reduce 18 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 128
+ def _reduce_19( val, _values, result )
+ leaf = AST::Word.new(
+ :line => @lexer.line,
+ :value => val[0]
+ )
+ result = AST::Selector.new(
+ :pin => "?",
+ :line => @lexer.line,
+ :param => leaf,
+ :value => val[2]
+ )
+ result
+ end
+.,.,
+
+ # reduce 20 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 131
+ def _reduce_21( val, _values, result )
+ result = val[1]
+ result
+ end
+.,.,
+
+ # reduce 22 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 142
+ def _reduce_23( val, _values, result )
+ if val[0].is_a?(Array)
+ val[0].push(val[1])
+ result = val[0]
+ else
+ result = AST::ASTArray.new([val[0],val[1]])
+ end
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 150
+ def _reduce_24( val, _values, result )
+ result = AST::FunctionCall.new(
+ :pin => '()',
+ :name => AST::Word.new(:value => val[0], :line => @lexer.line),
+ :values => val[2]
+ )
+ result
+ end
+.,.,
+
+module_eval <<'.,.,', 'grammar.ra', 156
+ def _reduce_25( val, _values, result )
+ result = FunctionDef.new(
+ :pin => '()',
+ :name => val[0]
+ )
+ result
+ end
+.,.,
+
+ # reduce 26 omitted
+
+module_eval <<'.,.,', 'grammar.ra', 158
+ def _reduce_27( val, _values, result )
+ result = nil
+ result
+ end
+.,.,
+
+ def _reduce_none( val, _values, result )
+ result
+ end
+
+ end # class Parser
+
+ end # module Parser
+
+end # module Blink
diff --git a/lib/blink/selector.rb b/lib/blink/selector.rb
new file mode 100644
index 000000000..51e82b09d
--- /dev/null
+++ b/lib/blink/selector.rb
@@ -0,0 +1,82 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+require 'blink/fact'
+
+module Blink
+ #---------------------------------------------------------------
+ # this class will provide something like a 'select' statement, but it will
+ # return a value
+ # it will be used something like this:
+ # value = Selector.new(
+ # proc { test() } => value,
+ # proc { test2() } => value2,
+ # )
+
+ # each test gets evaluated in turn; the first one to return true has its
+ # value returned as the value of the statement
+ # this will be used to provide abstraction in objects, but it's currently
+ # unused
+
+ class Selector < Array
+ attr_accessor :default
+
+ def add(value,&block)
+ option = Option.new(value,&block)
+ @ohash[value] = option
+ @oarray.push(option)
+ end
+
+ def evaluate
+ @oarray.each { |option|
+ if option.true?
+ return option.value
+ end
+ }
+ return nil
+ end
+
+ # we have to support providing different values based on
+ # different criteria, e.g., default is X, SunOS gets Y, and
+ # host Yayness gets Z.
+ # thus, no invariant
+ def initialize
+ @oarray = []
+ @ohash = {}
+
+ if block_given?
+ yield self
+ end
+ end
+
+ def to_s
+ return self.evaluate()
+ end
+
+ class Option
+ attr_accessor :value, :test, :invariant
+
+ def initialize(value,&block)
+ @value = value
+ @test = block
+ end
+
+ def to_s
+ if self.evaluate
+ return value
+ end
+ end
+
+ def true?
+ unless @test.is_a?(Proc)
+ raise "Cannot yet evaluate non-code tests"
+ end
+
+ return @test.call()
+ end
+ end
+ #---------------------------------------------------------------
+ end
+end