Domain-specific language

I'm writing an application that controls a group/cluster of linux
computers/nodes. I need to have a configuration file that lists the
nodes in the cluster.

It would be neat if the configuration file was in Ruby. If I had
nodes named node1 and node3, the configuration file could look a
little like:

node :node1 do
  ip 192.whatever
  title "Node 1"
end

node :node2 do
  ip 192.whatever
  title "Node 2"
end

So, is there some standard Ruby idiom for how to read a file and
execute the code? Just load the file and instance_eval it?

That's what SwitchTower does. I'm sure there's many other ways to do it, but it works well enough.

- Jamis

···

On Aug 18, 2005, at 3:40 PM, Joe Van Dyk wrote:

I'm writing an application that controls a group/cluster of linux
computers/nodes. I need to have a configuration file that lists the
nodes in the cluster.

It would be neat if the configuration file was in Ruby. If I had
nodes named node1 and node3, the configuration file could look a
little like:

node :node1 do
  ip 192.whatever
  title "Node 1"
end

node :node2 do
  ip 192.whatever
  title "Node 2"
end

So, is there some standard Ruby idiom for how to read a file and
execute the code? Just load the file and instance_eval it?

harp:~ > cat a.rb
     config = <<-config
       nodes :
         1 :
           ip : 192.whatever
           title : node 1
         2 :
           ip : 192.whatever
           title : node 2
     config

     require 'yaml'

     config = YAML::load config

     config['nodes'].each do |nid, node|
       puts "node <#{ nid }> => <#{ node.inspect }>"
     end

     harp:~ > ruby a.rb
     node <1> => <{"title"=>"node 1", "ip"=>"192.whatever"}>
     node <2> => <{"title"=>"node 2", "ip"=>"192.whatever"}>

so all you have to do is 'YAML::load(IO::read(configfile))'.

what kind of clustering are you working with?

you may, or may not, find this useful:

   http://raa.ruby-lang.org/project/rq/
   Linux Clustering with Ruby Queue: Small Is Beautiful | Linux Journal

cheers.

-a

···

On Fri, 19 Aug 2005, Joe Van Dyk wrote:

I'm writing an application that controls a group/cluster of linux
computers/nodes. I need to have a configuration file that lists the
nodes in the cluster.

It would be neat if the configuration file was in Ruby. If I had
nodes named node1 and node3, the configuration file could look a
little like:

node :node1 do
ip 192.whatever
title "Node 1"
end

node :node2 do
ip 192.whatever
title "Node 2"
end

So, is there some standard Ruby idiom for how to read a file and
execute the code? Just load the file and instance_eval it?

--

email :: ara [dot] t [dot] howard [at] noaa [dot] gov
phone :: 303.497.6469
Your life dwells amoung the causes of death
Like a lamp standing in a strong breeze. --Nagarjuna

===============================================================================

Thanks! I'll check it out.

So, say I have the following class

class ClusterManager

  def load_config_file config_file
    instance_eval File.read(config_file)
  end

  def node node_id, &block
    # What goes here?
  end

end

Or should I set it up differently?

···

On 8/18/05, Jamis Buck <jamis@37signals.com> wrote:

On Aug 18, 2005, at 3:40 PM, Joe Van Dyk wrote:

> I'm writing an application that controls a group/cluster of linux
> computers/nodes. I need to have a configuration file that lists the
> nodes in the cluster.
>
> It would be neat if the configuration file was in Ruby. If I had
> nodes named node1 and node3, the configuration file could look a
> little like:
>
> node :node1 do
> ip 192.whatever
> title "Node 1"
> end
>
> node :node2 do
> ip 192.whatever
> title "Node 2"
> end
>
> So, is there some standard Ruby idiom for how to read a file and
> execute the code? Just load the file and instance_eval it?

That's what SwitchTower does. I'm sure there's many other ways to do
it, but it works well enough.

Thanks for the pointers.

Essentially, there's a bunch of applications that need to be started
on a few machines.

Imagine you have 20 applications. You need to start those
applications on three different machines (nodes in a cluster). Some
applications will run on all machines, some applications will only run
on some machines. All the applications take many (20 or so)
environment options and command-line arguments and a user needs to be
able to change those options/arguments.

The users need to be able to select what application will get run on
what computer. They need to be able to make sure that the
applications haven't died, and if they have, to restart the
application. They need to be able to view log files for the
applications. Since these are real-time applications, the "cluster
manager application" that takes requests from the user's GUI and
distributes them to the computers in the cluster will probably need to
enforce certain restrictions on the user (i.e. can't run too many
cpu-intensive applications on one computer).

And because the applications and their env options and command-line
args change frequently, depending on what the user's objectives are,
it needs to be heavily driven by configuration files (that hopefully
are easy to change). I figure Ruby's syntax would be a good fit for
the configuration files.

The current design:

The GUI. The GUI is populated primarily from configuration files that
detail the available applications, their environment options and
command-line arguments.

The Cluster Manager. This connects the Node Managers to the GUIs.
Sends start and kill application requests from the GUIs to the Node
Managers, and sends node status updates from the nodes to the GUIs (so
the GUIs can see what's going on with each node and their
applications). Uses XML-RPC for communication (non-ruby clients will
need to access this).

The Node Manager. Starts and Kills applications on a node and sends
status updates (what applications are running, the node load average,
stuff like that) to the Cluster Manager. Uses DRb for communication
with the Cluster Manager.

···

On 8/18/05, Ara.T.Howard <Ara.T.Howard@noaa.gov> wrote:

On Fri, 19 Aug 2005, Joe Van Dyk wrote:

> I'm writing an application that controls a group/cluster of linux
> computers/nodes. I need to have a configuration file that lists the
> nodes in the cluster.
>
> It would be neat if the configuration file was in Ruby. If I had
> nodes named node1 and node3, the configuration file could look a
> little like:
>
> node :node1 do
> ip 192.whatever
> title "Node 1"
> end
>
> node :node2 do
> ip 192.whatever
> title "Node 2"
> end
>
> So, is there some standard Ruby idiom for how to read a file and
> execute the code? Just load the file and instance_eval it?

     harp:~ > cat a.rb
     config = <<-config
       nodes :
         1 :
           ip : 192.whatever
           title : node 1
         2 :
           ip : 192.whatever
           title : node 2
     config

     require 'yaml'

     config = YAML::load config

     config['nodes'].each do |nid, node|
       puts "node <#{ nid }> => <#{ node.inspect }>"
     end

     harp:~ > ruby a.rb
     node <1> => <{"title"=>"node 1", "ip"=>"192.whatever"}>
     node <2> => <{"title"=>"node 2", "ip"=>"192.whatever"}>

so all you have to do is 'YAML::load(IO::read(configfile))'.

what kind of clustering are you working with?

you may, or may not, find this useful:

   http://raa.ruby-lang.org/project/rq/
   Linux Clustering with Ruby Queue: Small Is Beautiful | Linux Journal

node :node1 do
  ip 192.whatever
  title "Node 1"
end

node :node2 do
  ip 192.whatever
  title "Node 2"
end

<snip>

class ClusterManager

  def load_config_file config_file
    instance_eval File.read(config_file)
  end

  def node node_id, &block
    # What goes here?
  end

end

I'd think similarly, but a bit different in that
ClusterManager::Builder should be the one doing the instance eval:

class ClusterManager
  attr_accessor :nodes

  def initialize
    @nodes =
  end

  def describe
    @nodes.collect{ |node| node.describe }.join("\n")
  end

  class Node
    attr_accessor :id, :ip, :title

    def initialize( id )
      @id = id
    end

    def describe
      "#@id -> #@ip \"#@title\""
    end

    class Builder
      def process( node, dsl )
        @node = node
        instance_eval &dsl
      end

      def ip( value )
        @node.ip = value
      end

      def title( value )
        @node.title = value
      end
    end
  end

  class Builder
    def process( manager, dsl )
      @manager = manager
      @node_builder = Node::Builder.new
      instance_eval &dsl
    end

    def node( node_id, &block )
      node = Node.new( node_id )
      @node_builder.process( node, block )
      @manager.nodes << node
    end
  end
end

dsl = lambda{
  node :node1 do
    ip '192.whatever'
    title 'Node 1'
  end

  node :node2 do
    ip '192.whatever'
    title 'Node 2'
  end
}

manager = ClusterManager.new
builder = ClusterManager::Builder.new
builder.process( manager, dsl )

puts manager.describe

···

###############

A few caveats about the above:

1) You'll notice I changed the DSL a little (quoted the 192.whatever
values). That was simply for brevity in this illustration.

2) My Builders require the argument to process be lambdas (Procs) not
strings. This was because I didn't want to complicate things with
switches on whether to use the prefix & or not. But it would be as
simple as an if/else to hide the lambda/string distinction from the
"user" (which will be yourself, not the person writing in the DSL)
inside the Builder. Alternatively, you can take a string and build it
into a lambda for the Builder via eval("lambda{ #{dsl} }"). I wouldn't
necessarily recommend that though (with all the evils of eval).

Jacob Fugal

Joe Van Dyk wrote:

I'm writing an application that controls a group/cluster of linux
computers/nodes. I need to have a configuration file that lists the
nodes in the cluster.

It would be neat if the configuration file was in Ruby. If I had
nodes named node1 and node3, the configuration file could look a
little like:

node :node1 do
  ip 192.whatever
  title "Node 1"
end

node :node2 do
  ip 192.whatever
  title "Node 2"
end

So, is there some standard Ruby idiom for how to read a file and
execute the code? Just load the file and instance_eval it?

That's what SwitchTower does. I'm sure there's many other ways to do
it, but it works well enough.

Thanks! I'll check it out.

So, say I have the following class

class ClusterManager

  def load_config_file config_file
    instance_eval File.read(config_file)
  end

  def node node_id, &block
    # What goes here?
  end

end

Or should I set it up differently?

Here's how I'd do it;

class ClusterManager

  def self.load_config_file config_file
    cm = new
    cm.instance_eval File.read(config_file)
    cm
  end

  def initialize ...
  # and other CM methods

  def node node_id, &block
    # What goes here?
  end

  class Node
    ...
  end
end

Then you can do

cm = ClusterManager.load_config_file "cluster.conf"

Instead of class Node you could also use Hashes. Then you can have

node(
  :name => "foo",
  :size => 10,
  ...
)

Kind regards

    robert

···

On 8/18/05, Jamis Buck <jamis@37signals.com> wrote:

On Aug 18, 2005, at 3:40 PM, Joe Van Dyk wrote:

Thank you for your ideas. I've adapted it somewhat for parts of my
application, but I'm running into problems.

I've got the following configuration file:

option :display do
  display :text, :size => 20, :title => "DISPLAY"
  value :DISPLAY, :default => ENV['DISPLAY'] || 'localhost:0'
end

argument :xterm_title do
  display :text, :size => 20, :title => "Xterm Title"
  value "-T", :default => "You are on a xterm!"
end

argument :xterm_text_color do
  display :text, :size => 20, :title => "Xterm Font Color"
  value "-fg", :default => "red"
end

application :xterm do
  executable "/usr/X11R6/bin/xterm"
  title :Xterm
  node :fatire
  options :display
  arguments :xterm_text_color, :xterm_title
end

application :xeyes do
  executable "/usr/X11R6/bin/xeyes"
  title :Xeyes
  node :fatire
  options :display
end

Should be self-explanatory. I have some applications that take env
options and command-line arguments, and I want to have a very readable
and configurable file for defining those applications, options, and
arguments (and some other things). The 'node' option for the
application is the machine that the application should be started on.
The 'display' option for the options/arguments state how the GUI
should display the option/argument.

My problem is trying to adapt your solution to fit something like
this. There would be a lot of duplication if I had Builders for
applications, nodes, options, and arguments. And I'm also having some
difficulties getting each Application object to know what options and
arguments it should have.

I considered using YAML for the configuration file format, but I'm
still leaning towards having a pure Ruby file.

Thoughts are greatly appreciated! This is my first time trying to do
this type of programming, so it's a little weird.

···

On 8/18/05, Jacob Fugal <lukfugl@gmail.com> wrote:

> node :node1 do
> ip 192.whatever
> title "Node 1"
> end
>
> node :node2 do
> ip 192.whatever
> title "Node 2"
> end

<snip>

> class ClusterManager
>
> def load_config_file config_file
> instance_eval File.read(config_file)
> end
>
> def node node_id, &block
> # What goes here?
> end
>
> end

I'd think similarly, but a bit different in that
ClusterManager::Builder should be the one doing the instance eval:

class ClusterManager
  attr_accessor :nodes

  def initialize
    @nodes =
  end

  def describe
    @nodes.collect{ |node| node.describe }.join("\n")
  end

  class Node
    attr_accessor :id, :ip, :title

    def initialize( id )
      @id = id
    end

    def describe
      "#@id -> #@ip \"#@title\""
    end

    class Builder
      def process( node, dsl )
        @node = node
        instance_eval &dsl
      end

      def ip( value )
        @node.ip = value
      end

      def title( value )
        @node.title = value
      end
    end
  end

  class Builder
    def process( manager, dsl )
      @manager = manager
      @node_builder = Node::Builder.new
      instance_eval &dsl
    end

    def node( node_id, &block )
      node = Node.new( node_id )
      @node_builder.process( node, block )
      @manager.nodes << node
    end
  end
end

dsl = lambda{
  node :node1 do
    ip '192.whatever'
    title 'Node 1'
  end

  node :node2 do
    ip '192.whatever'
    title 'Node 2'
  end
}

manager = ClusterManager.new
builder = ClusterManager::Builder.new
builder.process( manager, dsl )

puts manager.describe

###############

A few caveats about the above:

1) You'll notice I changed the DSL a little (quoted the 192.whatever
values). That was simply for brevity in this illustration.

2) My Builders require the argument to process be lambdas (Procs) not
strings. This was because I didn't want to complicate things with
switches on whether to use the prefix & or not. But it would be as
simple as an if/else to hide the lambda/string distinction from the
"user" (which will be yourself, not the person writing in the DSL)
inside the Builder. Alternatively, you can take a string and build it
into a lambda for the Builder via eval("lambda{ #{dsl} }"). I wouldn't
necessarily recommend that though (with all the evils of eval).

I would also like to be able to 'group' things together, like

group :xterm_options_arguments do
  arguments :xterm_title, :xterm_text_color
  options :display
end

application :xterm do
  group :xterm_options_arguments
  title :Xterm
  ...
end

So that applications with common options and arguments can specify a grouping.

(btw, an 'argument' is a command-line argument, like 'ls -F'... the -F
is an argument. An option is something like "DISPLAY=my_machine:1
xeyes".. the DISPLAY is an option).

···

On 9/7/05, Joe Van Dyk <joevandyk@gmail.com> wrote:

On 8/18/05, Jacob Fugal <lukfugl@gmail.com> wrote:
> > node :node1 do
> > ip 192.whatever
> > title "Node 1"
> > end
> >
> > node :node2 do
> > ip 192.whatever
> > title "Node 2"
> > end
>
> <snip>
>
> > class ClusterManager
> >
> > def load_config_file config_file
> > instance_eval File.read(config_file)
> > end
> >
> > def node node_id, &block
> > # What goes here?
> > end
> >
> > end
>
> I'd think similarly, but a bit different in that
> ClusterManager::Builder should be the one doing the instance eval:
>
> class ClusterManager
> attr_accessor :nodes
>
> def initialize
> @nodes =
> end
>
> def describe
> @nodes.collect{ |node| node.describe }.join("\n")
> end
>
> class Node
> attr_accessor :id, :ip, :title
>
> def initialize( id )
> @id = id
> end
>
> def describe
> "#@id -> #@ip \"#@title\""
> end
>
> class Builder
> def process( node, dsl )
> @node = node
> instance_eval &dsl
> end
>
> def ip( value )
> @node.ip = value
> end
>
> def title( value )
> @node.title = value
> end
> end
> end
>
> class Builder
> def process( manager, dsl )
> @manager = manager
> @node_builder = Node::Builder.new
> instance_eval &dsl
> end
>
> def node( node_id, &block )
> node = Node.new( node_id )
> @node_builder.process( node, block )
> @manager.nodes << node
> end
> end
> end
>
> dsl = lambda{
> node :node1 do
> ip '192.whatever'
> title 'Node 1'
> end
>
> node :node2 do
> ip '192.whatever'
> title 'Node 2'
> end
> }
>
> manager = ClusterManager.new
> builder = ClusterManager::Builder.new
> builder.process( manager, dsl )
>
> puts manager.describe
>
> ###############
>
> A few caveats about the above:
>
> 1) You'll notice I changed the DSL a little (quoted the 192.whatever
> values). That was simply for brevity in this illustration.
>
> 2) My Builders require the argument to process be lambdas (Procs) not
> strings. This was because I didn't want to complicate things with
> switches on whether to use the prefix & or not. But it would be as
> simple as an if/else to hide the lambda/string distinction from the
> "user" (which will be yourself, not the person writing in the DSL)
> inside the Builder. Alternatively, you can take a string and build it
> into a lambda for the Builder via eval("lambda{ #{dsl} }"). I wouldn't
> necessarily recommend that though (with all the evils of eval).

Thank you for your ideas. I've adapted it somewhat for parts of my
application, but I'm running into problems.

I've got the following configuration file:

option :display do
  display :text, :size => 20, :title => "DISPLAY"
  value :DISPLAY, :default => ENV['DISPLAY'] || 'localhost:0'
end

argument :xterm_title do
  display :text, :size => 20, :title => "Xterm Title"
  value "-T", :default => "You are on a xterm!"
end

argument :xterm_text_color do
  display :text, :size => 20, :title => "Xterm Font Color"
  value "-fg", :default => "red"
end

application :xterm do
  executable "/usr/X11R6/bin/xterm"
  title :Xterm
  node :fatire
  options :display
  arguments :xterm_text_color, :xterm_title
end

application :xeyes do
  executable "/usr/X11R6/bin/xeyes"
  title :Xeyes
  node :fatire
  options :display
end

Should be self-explanatory. I have some applications that take env
options and command-line arguments, and I want to have a very readable
and configurable file for defining those applications, options, and
arguments (and some other things). The 'node' option for the
application is the machine that the application should be started on.
The 'display' option for the options/arguments state how the GUI
should display the option/argument.

My problem is trying to adapt your solution to fit something like
this. There would be a lot of duplication if I had Builders for
applications, nodes, options, and arguments. And I'm also having some
difficulties getting each Application object to know what options and
arguments it should have.

I considered using YAML for the configuration file format, but I'm
still leaning towards having a pure Ruby file.

Thoughts are greatly appreciated! This is my first time trying to do
this type of programming, so it's a little weird.

And grouping of applications would also be nice:

application_group :all_x_apps
  applications :xterm, :xeyes
  display :checkbox, :title => "Start all X applications"
end

So, in the GUI, there would be a checkbox titled "Start all
applications" that would start the xterm and xeyes applications.

In other words, I'd like to be able to do grouping of various things
in this configuration file.

···

On 9/7/05, Joe Van Dyk <joevandyk@gmail.com> wrote:

On 9/7/05, Joe Van Dyk <joevandyk@gmail.com> wrote:
> On 8/18/05, Jacob Fugal <lukfugl@gmail.com> wrote:
> > > node :node1 do
> > > ip 192.whatever
> > > title "Node 1"
> > > end
> > >
> > > node :node2 do
> > > ip 192.whatever
> > > title "Node 2"
> > > end
> >
> > <snip>
> >
> > > class ClusterManager
> > >
> > > def load_config_file config_file
> > > instance_eval File.read(config_file)
> > > end
> > >
> > > def node node_id, &block
> > > # What goes here?
> > > end
> > >
> > > end
> >
> > I'd think similarly, but a bit different in that
> > ClusterManager::Builder should be the one doing the instance eval:
> >
> > class ClusterManager
> > attr_accessor :nodes
> >
> > def initialize
> > @nodes =
> > end
> >
> > def describe
> > @nodes.collect{ |node| node.describe }.join("\n")
> > end
> >
> > class Node
> > attr_accessor :id, :ip, :title
> >
> > def initialize( id )
> > @id = id
> > end
> >
> > def describe
> > "#@id -> #@ip \"#@title\""
> > end
> >
> > class Builder
> > def process( node, dsl )
> > @node = node
> > instance_eval &dsl
> > end
> >
> > def ip( value )
> > @node.ip = value
> > end
> >
> > def title( value )
> > @node.title = value
> > end
> > end
> > end
> >
> > class Builder
> > def process( manager, dsl )
> > @manager = manager
> > @node_builder = Node::Builder.new
> > instance_eval &dsl
> > end
> >
> > def node( node_id, &block )
> > node = Node.new( node_id )
> > @node_builder.process( node, block )
> > @manager.nodes << node
> > end
> > end
> > end
> >
> > dsl = lambda{
> > node :node1 do
> > ip '192.whatever'
> > title 'Node 1'
> > end
> >
> > node :node2 do
> > ip '192.whatever'
> > title 'Node 2'
> > end
> > }
> >
> > manager = ClusterManager.new
> > builder = ClusterManager::Builder.new
> > builder.process( manager, dsl )
> >
> > puts manager.describe
> >
> > ###############
> >
> > A few caveats about the above:
> >
> > 1) You'll notice I changed the DSL a little (quoted the 192.whatever
> > values). That was simply for brevity in this illustration.
> >
> > 2) My Builders require the argument to process be lambdas (Procs) not
> > strings. This was because I didn't want to complicate things with
> > switches on whether to use the prefix & or not. But it would be as
> > simple as an if/else to hide the lambda/string distinction from the
> > "user" (which will be yourself, not the person writing in the DSL)
> > inside the Builder. Alternatively, you can take a string and build it
> > into a lambda for the Builder via eval("lambda{ #{dsl} }"). I wouldn't
> > necessarily recommend that though (with all the evils of eval).
>
> Thank you for your ideas. I've adapted it somewhat for parts of my
> application, but I'm running into problems.
>
> I've got the following configuration file:
>
> option :display do
> display :text, :size => 20, :title => "DISPLAY"
> value :DISPLAY, :default => ENV['DISPLAY'] || 'localhost:0'
> end
>
> argument :xterm_title do
> display :text, :size => 20, :title => "Xterm Title"
> value "-T", :default => "You are on a xterm!"
> end
>
> argument :xterm_text_color do
> display :text, :size => 20, :title => "Xterm Font Color"
> value "-fg", :default => "red"
> end
>
>
> application :xterm do
> executable "/usr/X11R6/bin/xterm"
> title :Xterm
> node :fatire
> options :display
> arguments :xterm_text_color, :xterm_title
> end
>
> application :xeyes do
> executable "/usr/X11R6/bin/xeyes"
> title :Xeyes
> node :fatire
> options :display
> end
>
>
> Should be self-explanatory. I have some applications that take env
> options and command-line arguments, and I want to have a very readable
> and configurable file for defining those applications, options, and
> arguments (and some other things). The 'node' option for the
> application is the machine that the application should be started on.
> The 'display' option for the options/arguments state how the GUI
> should display the option/argument.
>
> My problem is trying to adapt your solution to fit something like
> this. There would be a lot of duplication if I had Builders for
> applications, nodes, options, and arguments. And I'm also having some
> difficulties getting each Application object to know what options and
> arguments it should have.
>
> I considered using YAML for the configuration file format, but I'm
> still leaning towards having a pure Ruby file.
>
> Thoughts are greatly appreciated! This is my first time trying to do
> this type of programming, so it's a little weird.
>

I would also like to be able to 'group' things together, like

group :xterm_options_arguments do
  arguments :xterm_title, :xterm_text_color
  options :display
end

application :xterm do
  group :xterm_options_arguments
  title :Xterm
  ...
end

So that applications with common options and arguments can specify a grouping.

(btw, an 'argument' is a command-line argument, like 'ls -F'... the -F
is an argument. An option is something like "DISPLAY=my_machine:1
xeyes".. the DISPLAY is an option).

> > Thank you for your ideas. I've adapted it somewhat for parts of my
> > application, but I'm running into problems.
> >
> > I've got the following configuration file:
> >
> > option :display do
> > display :text, :size => 20, :title => "DISPLAY"
> > value :DISPLAY, :default => ENV['DISPLAY'] || 'localhost:0'
> > end
> >
> > argument :xterm_title do
> > display :text, :size => 20, :title => "Xterm Title"
> > value "-T", :default => "You are on a xterm!"
> > end
> >
> > argument :xterm_text_color do
> > display :text, :size => 20, :title => "Xterm Font Color"
> > value "-fg", :default => "red"
> > end
> >
> >
> > application :xterm do
> > executable "/usr/X11R6/bin/xterm"
> > title :Xterm
> > node :fatire
> > options :display
> > arguments :xterm_text_color, :xterm_title
> > end
> >
> > application :xeyes do
> > executable "/usr/X11R6/bin/xeyes"
> > title :Xeyes
> > node :fatire
> > options :display
> > end
> >
> >
> > Should be self-explanatory. I have some applications that take env
> > options and command-line arguments, and I want to have a very readable
> > and configurable file for defining those applications, options, and
> > arguments (and some other things). The 'node' option for the
> > application is the machine that the application should be started on.
> > The 'display' option for the options/arguments state how the GUI
> > should display the option/argument.
> >
> > My problem is trying to adapt your solution to fit something like
> > this. There would be a lot of duplication if I had Builders for
> > applications, nodes, options, and arguments. And I'm also having some
> > difficulties getting each Application object to know what options and
> > arguments it should have.
> >
> > I considered using YAML for the configuration file format, but I'm
> > still leaning towards having a pure Ruby file.
> >
> > Thoughts are greatly appreciated! This is my first time trying to do
> > this type of programming, so it's a little weird.
> >
>
>
> I would also like to be able to 'group' things together, like
>
> group :xterm_options_arguments do
> arguments :xterm_title, :xterm_text_color
> options :display
> end
>
> application :xterm do
> group :xterm_options_arguments
> title :Xterm
> ...
> end
>
> So that applications with common options and arguments can specify a grouping.
>
> (btw, an 'argument' is a command-line argument, like 'ls -F'... the -F
> is an argument. An option is something like "DISPLAY=my_machine:1
> xeyes".. the DISPLAY is an option).

And grouping of applications would also be nice:

application_group :all_x_apps
  applications :xterm, :xeyes
  display :checkbox, :title => "Start all X applications"
end

So, in the GUI, there would be a checkbox titled "Start all
applications" that would start the xterm and xeyes applications.

In other words, I'd like to be able to do grouping of various things
in this configuration file.

Here's the start of what I have so far. How's it look?

require 'test/unit'

class ConfigLoader
  attr_accessor :configuration, :applications, :options

  def initialize
    @applications = Hash.new
    @options = Hash.new
  end

  def process configuration
    instance_eval configuration
  end

  def get_application_options application_id
    @options.values.find_all do |option|
      if option.option_type == :env_option
        @applications[application_id].options.include? option.option_id
      end
    end
  end

  def get_application_arguments application_id
    @options.values.find_all do |option|
      if option.option_type == :command_line_argument
        @applications[application_id].options.include? option.option_id
      end
    end
  end

  def application application_id, &block
    application = Application.new application_id
    @applications[application_id] = application
    application.instance_eval &block
  end

  def option option_id, &block
    option = Option.new option_id, :env_option
    @options[option_id] = option
    option.instance_eval &block
  end

  def argument option_id, &block
    option = Option.new option_id, :command_line_argument
    @options[option_id] = option
    option.instance_eval &block
  end
  class Option
    attr_accessor :name, :value, :default, :option_id, :option_type
    def initialize option_id, option_type
      @option_id = option_id
      @option_type = option_type
      @name = nil
      @value = nil
    end

    def set name, args
      @name = name
      @value = @default = args[:default]
    end
  end

  class Application
    attr_accessor :executable, :application_id, :title, :node, :options
    def initialize application_id
      @application_id = application_id
      @executable = nil
      @title = nil
      @node = nil
      @options =
    end

    def executable value=nil
      @executable = value if value
      @executable
    end

    def title value=nil
      @title = value if value
      @title
    end

    def node value=nil
      @node = value if value
      @node
    end

    def options *args
      @options = args if args.size > 0
      @options
    end

  end
end

class TestConfigLoader < Test::Unit::TestCase

  def test_process
    a = ConfigLoader.new
    a.process sample_configuration

    # Test to see that the application stuff was set ok.
    assert_equal "/usr/X11R6/bin/xeyes", a.applications[:xeyes].executable
    assert_equal [:display], a.applications[:xeyes].options
    assert_equal :fatire, a.applications[:xeyes].node
    assert_equal :Xeyes, a.applications[:xeyes].title

    # Test to see that the options were set ok.
    assert_equal :DISPLAY, a.options[:display].name
    # Not sure how to properly test these... they depend on your env
    # and if your DISPLAY is not set, then it should be something else
    # ('localhost:0'). So I'd be duplicating logic in the tests.
    #assert_equal "vandyk-j:0", a.options[:display].default
    #assert_equal "vandyk-j:0", a.options[:display].value

    # Test to see that the env options are ok.
    assert_equal [a.options[:display]], a.get_application_options(:xeyes)
    assert_equal [a.options[:display]], a.get_application_options(:xterm)

    # Test to see that the command line arguments are ok.
    assert_equal [a.options[:xterm_font_color], a.options[:xterm_title]],
                  a.get_application_arguments(:xterm)
  end

  # A sample configuration file
  def sample_configuration
   <<-EOF
option :display do
  #display :text, :size => 20, :title => "DISPLAY"
  set :DISPLAY, :default => ENV['DISPLAY'] || 'localhost:0'
end

argument :xterm_title do
  #display :text, :size => 20, :title => "Xterm Title"
  set "-T", :default => "You are on a xterm!"
end

argument :xterm_font_color do
  #display :text, :size => 20, :title => "Xterm Font Color"
  set "-fg", :default => "red"
end

application :xterm do
  executable "/usr/X11R6/bin/xterm"
  title :Xterm
  node :fatire
  options :display, :xterm_font_color, :xterm_title
end

application :xeyes do
  executable "/usr/X11R6/bin/xeyes"
  title :Xeyes
  node :fatire
  options :display
end
   EOF
  end
end