From: Han Holl Date: 2003-03-14T06:23:35+09:00 Subject: Expect for ruby This is a multi-part message in MIME format. --------------070003020904030806010803 Content-Type: text/plain; charset=us-ascii; format=flowed Content-Transfer-Encoding: 7bit Inspired by a posting from Hugh Sasse a couple of days ago, and in an ongoing effort to remove languages that are in very infrequent use on our systems, I've written an expect class for ruby. It has a couple of useful features, all coming directly from Don Libes expect, like expect_before and after, and exp_internal. Here's a snippet to give you a feel of how it can be used, and how 'close' it feels to the original, at least when using a class Expect singleton. # The most comfortable way of using this expect is by writing a # Expect class singleton, as in the setpasswd example. def main $expect_debug = false require "expectcls.rb" expect = Expect.new class << expect def script(passw) add { |x| puts "Matched #{x}" } # default action expect_after(:EOF, :TIMEOUT) { |p| raise p.to_s } expect("password: ") sleep(0.5) send("#{passw}\r") expect("Retype") expect("password: ") sleep(0.5) send("#{passw}\r") expect("successfully") end end expect.spawn("/usr/bin/passwd #{ARGV[0]}") do |exp| exp.script(ARGV[1]) end end main I hope this is of some interest to some people on this list. Concrete question: there are a couple of helper classes and modules, that should be as invisible as possible. Hoe does one go about that? In C++ I would make a lot of stuff private, and declare a bunch of friends, but Ruby doesn't support those concepts. Cheers, Han Holl --------------070003020904030806010803 Content-Type: text/plain; name="expectcls.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="expectcls.rb" #!/usr/bin/ruby =begin Expect can be used to control interactive processes. It is inspired by a posting by Hugh Sasse a while back. (and of course by Don Libes original expect) but does not try to be compatible with any existing function. I haven't tried to put everything in it that the original expect has, but just the stuff I personally use expect for: automated password setting, analizing multiple logfiles real time by spawning 'tail -f', and such. The most comfortable way of using this expect is by writing a Expect class singleton, as in the setpasswd example. It tries to find patterns in an incoming stream, and performs an action when a match is found. There are three sets of patterns/actions: the current working set, and sets to try before and after the working set. A pattern can be a Regexp, a String or one the symbols :EOF and :TIMEOUT An action is a Proc that will be called with the MatchData, the matching string or symbol as it's sole argument. Methods are provided to manipulate the three sets. Default initial timeout is 10 seconds, default initial action is nil. An nil action implies that no Proc is called, but that the matching result is returned. The basic execution loop is as follows: try to read as many characters as possible without waiting. If we got some, try the patterns. If we got none, and the last round did not produce a match, try to get more chars with timeout. An Errno::EIO error signals EOF Most methods accept a number of patterns and/or actions, and can be given a block. The parameterlist is processed as follows: If a pattern is directly followed by a Proc object, they get to form a pair. If a pattern is not followed by a Proc object, it gets associated with the block given, if any, and else with the default action. If no patterns are given at all, but just a block, the default action is set to that block. The class has the following instance methods: spawn(process) {|expect_obj| block } PTY.spawn process and execute block timeout(int) sets a new timeout value exp_internal(filename, mode, prefix) opens a file to log expect internals to. Use '/dev/tty' (on Unix) to log to the console. Prefix is handy to recognize this log if on screen. send(string) send the string to the spawned process. exp_continue Don't exit from expect function after a succesful match. expect_before(*args) [ { block ] expect_after(*args) [ { block ] add(*args) [ { block ] add patterns / actions to one of the three sets, according to the above rules clear erase patterns / actions from working set expect(*args) [ { block } ] Three variants: expect # without arguments or block uses the three current sets as patterns, and calls the associated block on a match expect(*args) # with or without block clears the current working set and constructs a new one from args expect { block } sets the block as new default, and uses the existing sets as patterns / actions. Same as: add { block } expect =end require "pty" class OutputSink def puts(s) end end $debug_out = $expect_debug ? $stderr : OutputSink.new # A class for holding the ordered list of pattern action pairs, like # those used by Don Libes' Expect program. Because they are # ordered, a Hash cannot be used. # class ExpectInstructions attr_accessor :default def initialize @instructions = Array.new @default = nil end # turn the supplied parameters into instructions and actions def add_params(*params, &block) if params.size == 0 @default = block if block else params.each_with_index do |param,i| if param.is_a?(Regexp) || param.is_a?(String) || param.is_a?(Symbol) action = params[i + 1] if ! action.is_a?(Proc) action = block ? block : @default end @instructions.push [param, action] end end end # instructions is now an ordered list of pattern, (action|nil) $debug_out.puts "@instructions = #{@instructions.inspect}" end def each_pair @instructions.each do |pair| yield(pair[0], pair[1]) end end def clear @instructions = Array.new end end class ExpectInstructionsArray def initialize(ar) @ar = ar end def each_pair(&block) @ar.each do |a| a.each_pair(&block) end end end module Expect_in attr_accessor :expect_internal, :buf, :eof, :timeout def exp_internal(wrt, filename = "/dev/tty", mode = "w", prefix = "* " ) @prefix = prefix @expect_internal = File.open(filename, mode) wrt.exp_internal(@expect_internal, prefix) if wrt end def read_avail rsl = "" if ! @eof begin while IO.select([self],nil,nil,0) rsl << getc end rescue Errno::EIO @eof = true self.close # trying to select after this raises en IOError end end rsl end def expect(pat) result = nil begin @exp_continue = false $debug_out.puts "start of catch block" result = nil matched = true catch(:block_called) do while true got = read_avail timedout = false if got.empty? && ! matched av = IO.select([self],nil,nil,@timeout) if av.nil? timedout = true got = "" else got = read_avail end end @buf << got @expect_internal.print "#{@prefix}Does #{@buf.dump} match:\n" if @expect_internal matched = false pats = false pat.each_pair do |pattern, aproc| pats = true case pattern when Regexp @expect_internal.print "#{@prefix} #{pattern.source.dump} ?" if @expect_internal if mat = pattern.match(@buf) ret = mat newbuf = mat.post_match end when String @expect_internal.print "#{@prefix} #{pattern.dump} ?" if @expect_internal if mat = @buf.index(pattern) newbuf = @buf[mat + pattern.size .. -1] ret = pattern end when Symbol @expect_internal.print "#{@prefix} #{pattern} ?" if @expect_internal if pattern == :EOF mat = @eof elsif pattern == :TIMEOUT mat = timedout else raise "Illegal symbol #{pattern}" end newbuf = buf ret = pattern else mat = nil end if mat @expect_internal.print " Yes\n" if @expect_internal $debug_out.puts("matched '#{mat[0].dump}'") if mat.is_a?(MatchData) if aproc aproc.call(ret) else result = ret end @buf = newbuf matched = true throw :block_called else @expect_internal.print " No\n" if @expect_internal end end if ! pats throw :block_called end end # end while true end # end catch (:block_done) $debug_out.puts "end of catch block Rsl:#{result}" return result if result $debug_out.puts "near end of while @exp_continue" end while @exp_continue nil end def exp_continue @exp_continue = true $debug_out.puts "exp_continue called" end end module Expect_out attr_accessor :expect_internal def exp_internal(h, prefix) @prefix = prefix @expect_internal = h end def send(str) @expect_internal.print("#{@prefix}Sending: #{str.dump}\n") if @expect_internal print(str) end end class Expect attr_accessor :rd, :wrt, :pid def initialize @before = ExpectInstructions.new @after = ExpectInstructions.new @workingset = ExpectInstructions.new end def spawn(process) trap('SIGCHLD','IGNORE') PTY.spawn(process) do |@rd, @wrt, @pid| PTY.protect_signal do # No longer necessary in 1.8 ? @wrt.sync = true @rd.extend(Expect_in) @wrt.extend(Expect_out) @rd.expect_internal = @wrt.expect_internal = nil @rd.buf = "" @rd.eof = false @rd.timeout = 10 yield self end end end def timeout(tim) @rd.timeout = tim if tim.is_a?(Fixnum) end def exp_internal(*args) @rd.exp_internal(@wrt, *args) end def expect(*pat, &block) @workingset.clear if pat.size > 0 @workingset.add_params(*pat, &block) @rd.expect(ExpectInstructionsArray.new([@before, @workingset, @after])) end def send(*args) @wrt.send(*args) end def exp_continue @rd.exp_continue end def expect_before(*params, &block) @before.add_params(*params, &block) end def expect_after(*params, &block) @after.add_params(*params, &block) end def add(*params, &block) @workingset.add_params(*params, &block) end def clear wh.clear end end if $0 == __FILE__ # This test does not run on 1.6.8 (RuntimeError) # On 1.8.0 preview it's fine # It's impossible to spawn short-lived processes on 1.6.8 expect = Expect.new class << expect def monitor date = Time.now.to_s.split exp_internal('/dev/tty', 'w') expect_before(:TIMEOUT) { raise 'Timeout' } date.each do |part| $stderr.puts("Exp: #{part}") expect(part) end expect(:EOF) begin expect("something more") rescue IOError if $!.message == "closed stream" puts "Passed" else raise end end end end expect.spawn('/bin/date') do |exp| exp.monitor end end --------------070003020904030806010803--