# # people use all sort of hacks to build dsl's in ruby, everything from # class_evals to module_evals and a billion method_missing hacks. i wanted to # present here what i believe to be the simplest and most robust method for # creating DSLs, it's suprisingly simply and offers all the advantages and # none of the disadvantages other methods might have. the dsl we'll use as an # example is one which allows running commands against the bash shell with # dsl methods for each shell command - although it's *only* for example our # dsl will have the interesting property that, unlike using ruby's built-in # system or backticks, our dsl will run commands against the same shell which # allows a more natural interaction with shell state persisting between calls. # # obviously we'll namespace the whole kit and kaboodle # module Bash # our shell class is s pretty naive one which simply talks to /bin/bash via # pipes. we take a little care to make sure we're shutdown nicely when # finished and/or at exit, and provide a simple mechanism for reading back # the stdout from a command run against the bash instance - note that we # have to ask bash to delim the output for us (____start... and ____stop...) # in the output stream since we are going to re-use the same shell for each # command and, otherwise, we'd have to way to distinquish the output of each # successive command. the only other important thing to note is that we use # a dsl to evaluate any given block - see DSL class below for gory details. # class Shell def initialize path = '/bin/bash', &block @shell = IO.popen path, 'r+' @dsl = DSL.new self @commandno = -1 @at_exit = lambda{ @at_exit = lambda{}; @shell.close } at_exit{ close } if block begin evaluate &block ensure close end end end def close @at_exit.call end def evaluate &block @dsl.send('__instance_eval__', &block) end def execute command n = ( @commandno += 1 ) start = "____start_#{ n }___" stop = "____stop_#{ n }___" @shell.puts "echo #{ start }" @shell.puts command @shell.puts "echo #{ stop }" @shell.flush stdout = [] while(( line = @shell.gets )) case line.chomp when Regexp.escape(start) next when Regexp.escape(stop) break else stdout << line end end stdout.join end end class DSL # of course this is the interesting bit, we do two things here: first we # alias all the instance methods for later use as underscore methods, in # otherwords # # is_a? => __is_a__? # class => __class__ # tainted? => __tainted__? # # etc. # # then we nuke just about all the methods, leaving behind only __send__, # __id__, etc. the final result is a blankslate object which actually has # 100% of the old methods, only as __burried_away_ones__. # # finally our initialize method wraps up our shell instance and holds as # an ivar for later use # instance_methods.each do |m| next if m[%r'^__'] src = m.to_s name, suffix = src.split %r/([?!])/ dst = "__#{ name }__#{ suffix }" alias_method dst, src end def initialize shell @shell = shell end # this is the meat - our dsl - notice how these, and *only* these methods # will be available to the dsl since all other methods were nuked above. # also notice that we are free to have methods on the shell with a more # complicated interface, less error checking, etc, since the dsl namespace # is *not* the same as the shell class namespace. also note that these # methods are all really dumb to implement this way, but it's only for the # purpose of the example - any sane person would use FileUtils or # similar. # def echo *strings command = [ "cat <<'hdoc'", strings.join("\n"), "hdoc", ].join("\n") puts @shell.execute(command) end def cd dir = '.' command = "cd #{ dir }" @shell.execute(command) end def pwd puts @shell.execute("pwd") end def set kvs = {} kvs.each{|k,v| @shell.execute "#{ k }=#{ v.to_s.inspect }"} end def get k puts @shell.execute("echo ${#{ k }}") end end end # we setup one top level method to bootstrap us into shell space, it just # delegates to the contructor # def bash(*a, &b) Bash::Shell.new(*a, &b) end # finally we can use the shell object, passing a block to force evaluation # inside the dsl # bash { echo 'and the bunnymen.' # prints 'and the bunnymen' cd '/' pwd # prints '/' set :x => 42 get :x # prints '42' }