Expect.rb -- possible patch

I have been wanting to use Ruby for expect-like applications, but
have had some difficulties because of wanting more facilities. I
have managed to modify expect.rb, from the
…/ruby-1.8.0/ext/pty/lib/expect.rb
to do what I want, and would like to tentatively suggest the
following patch. It needs much more testing, and is still “wet
paint” though it seems to do what I want.

So, what did I want?

  • I wanted to be able to look for several patterns at the same time,
    and respond accordingly.

  • I wanted to be able to repeat the cycle of looking for things,
    i.e. exp_continue (in Don Libes’ expect.

  • I wanted to specify the order in which patterns were tested for
    matching.

  • respond to a timeout, should that occur

I seem to have achieved these goals. I also wanted:

  • To be able to detect an EOF and respond accordingly

  • To implement interact, so the program could drive the remote
    system up to a point, then call on the user to drive it from
    thereon
    I have not achieved these goals, but cannot work on them now, for a
    while at least.

The patch is this (I’m using the =begin…=end delimiters for
familiarity and therefore clarity, since they are not used in the
code) :

=begin patch
— expect.rb.orig 2002-08-13 07:07:08.000000000 +0100
+++ expect.rb 2003-03-03 17:15:47.000008000 +0000
@@ -1,36 +1,149 @@
$expect_verbose = false

+# 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.

···

+#
+# it may be worth adding assoc and rassoc methods to this
+# class simply because of the structure of the instructions, but at
+# the moment I can see no utiltity for this.
+class ExpectInstructions
+

  • def initialize(*params)
  • @timeout = false
  • @instructions = Array.new
  • add_params(params)
  • $stderr.puts “@instructions = #{@instructions.inspect}”
  • end
  • turn the supplied parameters into instructions. The parameters

  • are a list of pattern, action pairs. Actions must be Proc

  • objects (because you can’t just add a block to an array.

  • Patterns will be Strings, Regexps, or the Symbol :TIMEOUT.

  • The symbol :EOF may be added later

  • It is thus possible to detect if an action has been missed out,

  • in which case the pattern will just be detected with no actual

  • work being done.

  • def add_params(*params)
  • params.flatten!
  • make all the patterns into regexp

  • params.each_with_index { |param,i|
  •  case param
    
  •  when String
    
  •    params[i] = Regexp.new(Regexp.quote(param))
    
  •  end
    
  • }
  • i = 0
  • while i < params.size
  •  case params[i]
    
  •  when Regexp, Symbol
    
  •    pattern = params[i]
    
  •    i += 1
    
  •    case params[i]
    
  •    when Proc
    
  •      action = params[i]
    
  •      i += 1
    
  •    else
    
  •      action = nil
    
  •    end
    
  •  end
    
  •  @instructions.push [pattern, action]
    
  • end
  • instructions is now an ordered list of pattern, (action|nil)

  • $stderr.puts “@instructions = #{@instructions.inspect}”
  • end
  • def each(&block)
  • @instructions.each(&block)
  • end
  • def each_pair
  • @instructions.each { |pair|
  •  yield pair[0], pair[1]
    
  • }
  • end
  • def has_pattern?(pat)
  • @instructions.assoc(pat)
  • end
  • def
  • @instructions.assoc(value)[1]
  • end

+end
+
class IO
def expect(pat,timeout=9999999)

  • buf = ‘’
  • case pat
  • when String
  •  e_pat = Regexp.new(Regexp.quote(pat))
    
  • when Regexp
  •  e_pat = pat
    
  • end
  • while true
  •  if IO.select([self],nil,nil,timeout).nil? then
    
  •    result = nil
    
  •    break
    
  •  end
    
  •  c = getc.chr
    
  •  buf << c
    
  •  if $expect_verbose
    
  •    STDOUT.print c
    
  •    STDOUT.flush
    
  • instructions = false # till we figure out otherwise
  • result = nil
  • begin
  •  @exp_continue = false
    
  •  buf = ''
    
  •  $stderr.puts "buf cleared"
    
  •  case pat
    
  •  when ExpectInstructions #of [pattern, block,...] form
    
  •    instructions = true
    
  •  when String
    
  •    e_pat = Regexp.new(Regexp.quote(pat))
    
  •  when Regexp
    
  •    e_pat = pat
     end
    
  •  if mat=e_pat.match(buf) then
    
  •    result = [buf,*mat.to_a[1..-1]]
    
  •    break
    
  •  $stderr.puts "start of catch block"
    
  •  catch (:block_called) do
    
  •    while true
    
  •      if IO.select([self],nil,nil,timeout).nil? then
    
  •        result = nil
    
  •        if instructions
    
  •          if pat.has_pattern?(:TIMEOUT)
    
  •            pat[:TIMEOUT].call()
    
  •          end
    
  •        elsif pat =~ /^TIMEOUT$/ or pat == :TIMEOUT
    
  •          if block_given?
    
  •            yield
    
  •          end
    
  •        end
    
  •        throw :block_called
    
  •      end
    
  •      c = getc.chr
    
  •      buf << c
    
  •      if $expect_verbose
    
  •        STDOUT.print c
    
  •        STDOUT.flush
    
  •      end
    
  •      if instructions
    
  •        pat.each_pair { |pattern, aproc|
    
  •          # $stderr.puts "pattern is #{pattern.inspect}"
    
  •          if mat=pattern.match(buf) then
    
  •            $stderr.puts "matched '#{buf}'"
    
  •            result = [buf,*mat.to_a[1..-1]]
    
  •            aproc.call(result)
    
  •            throw :block_called
    
  •          end
    
  •        }
    
  •      else
    
  •        if mat=e_pat.match(buf) then
    
  •          result = [buf,*mat.to_a[1..-1]]
    
  •          throw :block_called
    
  •        end
    
  •      end # end if instructions...else...
    
  •    end # end while true
    
  •  end # end catch (:block_done)
    
  •  $stderr.puts "end of catch block"
    
  •  unless instructions
    
  •    if block_given? then
    
  •      yield result
    
  •    else
    
  •      return result
    
  •    end
     end
    
  • end
  • if block_given? then
  •  yield result
    
  • else
  •  return result
    
  • end
  •  $stderr.puts "near end of while @exp_continue"
    
  • end while @exp_continue
    nil
    end
  • def exp_continue
  • @exp_continue = true
  • $stderr.puts “exp_continue called”
  • end
    end

=end patch

I needed some way to test this, so wrote

=begin try_expect.rb
#!/usr/local/bin/ruby -w

require "pty"
load “./expect.rb”

count = 0
$expect_verbose = true
PTY.spawn(“ruby -r debug simple.rb”) { |rd, wrt, pid|
wrt.sync = true
ins = ExpectInstructions.new(/\r\n:([^\r\n])[\r\n]$/, Proc.new {|r|
r.shift
location, string = r
count += 1
puts "location is {#{location}}"
puts "string is {#{string}}"
if string =~ /(\S+) ?[+
-]?=[^=]/
puts “at #{location}, #{$1} is assigned"
end
rd.exp_continue unless count > 10
})
ins.add_params(/(rdb:1)/, Proc.new {|x| wrt.puts “s”; rd.exp_continue})
ins.add_params(“Emacs support available.”, Proc.new {|x| rd.exp_continue})
rd.expect(ins)
rd.expect(”(rdb:1)")
$stderr.puts "old style expect(string) done"
wrt.puts "s"
rd.expect(/(rdb:1)/)
$stderr.puts "old style expect(regexp) done"
wrt.puts “s”
$stderr.puts Time.now
rd.expect(“TIMEOUT”,10)
$stderr.puts Time.now
$stderr.puts "Got timeout"
rd.expect(:TIMEOUT,10)
$stderr.puts Time.now
$stderr.puts “Got timeout”

}
=end try_expect.rb

which drives this trivial script:

=begin simple.rb
#!/usr/local/bin/ruby -w

$X = 5
x = 0
while 1
x += 1
sleep 3
end
=end simple.rb

Since the changes proposed at
http://www.rubygarden.org/ruby?RExpect
do not seem to have been integrated into 1.8.0 I have not attempted
to add any of them. If expect.rb is to be examinined in the light
of this post, it may well be worth considering those while “the lid
is off”.

The tests would be more rigorous, but I needed to get this to
a stage where I could use it for another task, which has to have
priority right now. So if this patch is received favourably I will
try to tighten up the tests before it is actually accepted. Think
of it as a discussion document for now.

I actually ran the tests using
ruby 1.6.7 (2002-03-01) [sparc-solaris2.9]
because the only difference between 1.6.x and 1.8.x was
result = [buf,*mat.to_a[1…-1]]
replacing
result = [buf,$1,$2,$3,$4,$5,$6,$7,$8,$9]
demonstrating that compatibility is not really an issue
so if this is accepted it could be backported if desired.

    HTH
    Hugh