class Expectr

Public: Expectr is an API to the functionality of Expect (see expect.nist.gov) implemented in ruby.

Expectr contrasts with Ruby’s built-in expect.rb by avoiding tying in with the IO class in favor of creating a new object entirely to allow for more granular control over the execution and display of the program being run.

Examples

# SSH Login to another machine
exp = Expectr.new('ssh user@example.com')
exp.expect("Password:")
exp.send('password')
exp.interact!(blocking: true)

# See if a web server is running on the local host, react accordingly
exp = Expectr.new('netstat -ntl|grep ":80 " && echo "WEB"', timeout: 1)
if exp.expeect("WEB")
  # Do stuff if we see 'WEB' in the output
else
  # Do other stuff
end

Constants

DEFAULT_BUFFER_SIZE
DEFAULT_CONSTRAIN
DEFAULT_FLUSH_BUFFER
DEFAULT_TIMEOUT
VERSION

Attributes

buffer[R]

Public: Returns the active buffer to match against

buffer_size[RW]

Public: Gets/sets the number of bytes to use for the internal buffer

constrain[RW]

Public: Gets/sets whether to constrain the buffer to the buffer size

discard[R]

Public: Returns the buffer discarded by the latest call to Expectr#expect

flush_buffer[RW]

Public: Gets/sets whether to flush program output to $stdout

pid[R]

Public: Returns the PID of the running process

timeout[RW]

Public: Gets/sets the number of seconds a call to Expectr#expect may last

Public Class Methods

new(cmd_args = '', args = {}) click to toggle source

Public: Initialize a new Expectr object. Spawns a sub-process and attaches to STDIN and STDOUT for the new process.

cmd_args - This may be either a Hash containing arguments (described below)

or a String or File Object referencing the application to launch
(assuming Child interface).  This argument, if not a Hash, will
be changed into the Hash { cmd: cmd_args }.  This argument will
be  merged with the args Hash, overriding any arguments
specified there.
This argument is kept around for the sake of backward
compatibility with extant Expectr scripts and may be deprecated
in the future.  (default: {})

args - A Hash used to specify options for the instance. (default: {}):

:timeout      - Number of seconds that a call to Expectr#expect
                has to complete (default: 30)
:flush_buffer - Whether to flush output of the process to the
                console (default: true)
:buffer_size  - Number of bytes to attempt to read from
                sub-process at a time.  If :constrain is true,
                this will be the maximum size of the internal
                buffer as well.  (default: 8192)
:constrain    - Whether to constrain the internal buffer from
                the sub-process to :buffer_size characters.
                (default: false)
:interface    - Interface Object to use when instantiating the
                new Expectr object. (default: Child)
# File lib/expectr.rb, line 82
def initialize(cmd_args = '', args = {})
  setup_instance
  parse_options(args)

  cmd_args = { cmd: cmd_args } unless cmd_args.is_a?(Hash)
  args.merge!(cmd_args)

  unless [:lambda, :adopt, :child].include?(args[:interface])
    args[:interface] = :child
  end

  self.extend self.class.const_get(args[:interface].capitalize)
  init_interface(args)

  Thread.new { output_loop }
end

Public Instance Methods

clear_buffer!() click to toggle source

Public: Clear output buffer.

Returns nothing.

# File lib/expectr.rb, line 227
def clear_buffer!
  @out_mutex.synchronize do
    @buffer.clear
  end
end
expect(pattern, recoverable = false) { |match| ... } click to toggle source

Public: Begin a countdown and search for a given String or Regexp in the output buffer, optionally taking further action based upon which, if any, match was found.

pattern - Object String or Regexp representing pattern for which to

search, or a Hash containing pattern -> Proc mappings to be
used in cases where multiple potential patterns should map
to distinct actions.

recoverable - Denotes whether failing to match the pattern should cause the

method to raise an exception (default: false)

Examples

exp.expect("this should exist")
# => MatchData

exp.expect("this should exist") do
  # ...
end

exp.expect(/not there/)
# Raises Timeout::Error

exp.expect(/not there/, true)
# => nil

hash = { "First possibility"  => -> { puts "option a" },
         "Second possibility" => -> { puts "option b" },
         default:             => -> { puts "called on timeout" } }
exp.expect(hash)

Returns a MatchData object once a match is found if no block is given Yields the MatchData object representing the match Raises TypeError if something other than a String or Regexp is given Raises Timeout::Error if a match isn’t found in time, unless recoverable

# File lib/expectr.rb, line 177
def expect(pattern, recoverable = false)
  return expect_procmap(pattern) if pattern.is_a?(Hash)

  match = nil
  pattern = Regexp.new(Regexp.quote(pattern)) if pattern.is_a?(String)
  unless pattern.is_a?(Regexp)
    raise(TypeError, Errstr::EXPECT_WRONG_TYPE)
  end

  match = watch_match(pattern, recoverable)
  block_given? ? yield(match) : match
end
expect_procmap(pattern_map) click to toggle source

Public: Begin a countdown and search for any of multiple possible patterns, performing designated actions upon success/failure.

pattern_map - Hash containing mappings between Strings or Regexps and

procedure objects.  Additionally, an optional action,
designated by :default or :timeout may be provided to specify
an action to take upon failure.

Examples

exp.expect_procmap({
  "option 1" => -> { puts "action 1" },
  /option 2/ => -> { puts "action 2" },
  :default   => -> { puts "default" }
})

Calls the procedure associated with the pattern provided.

# File lib/expectr.rb, line 207
def expect_procmap(pattern_map)
  pattern_map, pattern, recoverable = process_procmap(pattern_map)
  match = nil

  match = watch_match(pattern, recoverable)

  pattern_map.each do |s,p|
    if s.is_a?(Regexp)
      return p.call if s.match(match.to_s)
    end
  end

  pattern_map[:default].call unless pattern_map[:default].nil?
  pattern_map[:timeout].call unless pattern_map[:timeout].nil?
  nil
end
interact!(args = {}) click to toggle source

Public: Allow direct control of the running process from the controlling terminal, acting as a pass-through for the life of the process (or until the leave! method is called).

args - A Hash used to specify options to be used for interaction.

(default: {}):
:flush_buffer - explicitly set @flush_buffer to the value specified
:blocking     - Whether to block on this call or allow code
                execution to continue (default: false)

Returns the interaction Thread, calling join on it if :blocking is true.

# File lib/expectr.rb, line 110
def interact!(args = {})
  if @interact
    raise(ProcessError, Errstr::ALREADY_INTERACT)
  end

  @flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer]
  args[:blocking] ? interact_thread.join : interact_thread
end
interact?() click to toggle source

Public: Report whether or not current Expectr object is in interact mode.

Returns a boolean.

# File lib/expectr.rb, line 122
def interact?
  @interact
end
leave!() click to toggle source

Public: Cause the current Expectr object to leave interact mode.

Returns nothing.

# File lib/expectr.rb, line 129
def leave!
  @interact=false
end
puts(str = '') click to toggle source

Public: Wraps Expectr#send, appending a newline to the end of the string.

str - String to be sent to the active process. (default: ”)

Returns nothing.

# File lib/expectr.rb, line 138
def puts(str = '')
  send str + "\n"
end

Private Instance Methods

check_match(pattern) click to toggle source

Internal: Check for a match against a given pattern until a match is found. This method should be wrapped in a Timeout block or otherwise have some mechanism to break out of the loop.

pattern - String or Regexp containing the pattern for which to watch.

Returns a MatchData object containing the match found.

# File lib/expectr.rb, line 314
def check_match(pattern)
  match = nil
  @thread = Thread.current
  while match.nil?
    @out_mutex.synchronize do
      match = pattern.match(@buffer)
      if match.nil?
        raise Timeout::Error if @pid.zero?
        @out_mutex.sleep
      end
    end
  end
  match
ensure
  @thread = nil
end
force_utf8(buf) click to toggle source

Internal: Encode a String twice to force UTF-8 encoding, dropping problematic characters in the process.

buf - String to be encoded.

Returns the encoded String.

# File lib/expectr.rb, line 252
def force_utf8(buf)
  return buf if buf.valid_encoding?
  buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)
end
parse_options(args) click to toggle source

Internal: Initialize instance variables based upon arguments provided.

args - A Hash used to specify options for the new object (default: {}):

:timeout      - Number of seconds that a call to Expectr#expect has
                to complete.
:flush_buffer - Whether to flush output of the process to the
                console.
:buffer_size  - Number of bytes to attempt to read from sub-process
                at a time.  If :constrain is true, this will be the
                maximum size of the internal buffer as well.
:constrain    - Whether to constrain the internal buffer from the
                sub-process to :buffer_size.

Returns nothing.

# File lib/expectr.rb, line 271
def parse_options(args)
  @timeout = args[:timeout] || DEFAULT_TIMEOUT
  @buffer_size = args[:buffer_size] || DEFAULT_BUFFER_SIZE
  @constrain = args[:constrain] || DEFAULT_CONSTRAIN
  @flush_buffer = args[:flush_buffer]
  @flush_buffer = DEFAULT_FLUSH_BUFFER if @flush_buffer.nil?
end
print_buffer(buf) click to toggle source

Internal: Print buffer to $stdout if program output is expected to be echoed.

buf - String to be printed to $stdout.

Returns nothing.

process_output(buf) click to toggle source

Internal: Handle data from the interface, forcing UTF-8 encoding, appending it to the internal buffer, and printing it to $stdout if appropriate.

Returns nothing.

# File lib/expectr.rb, line 294
def process_output(buf)
  force_utf8(buf)
  print_buffer(buf)

  @out_mutex.synchronize do
    @buffer << buf
    if @constrain && @buffer.length > @buffer_size
      @buffer = @buffer[-@buffer_size..-1]
    end
    @thread.wakeup if @thread
  end
end
process_procmap(pattern_map) click to toggle source

Internal: Process a pattern to procedure mapping, producing a sanitized Hash, a unified Regexp and a boolean denoting whether an Exception should be raised upon timeout.

pattern_map - A Hash containing mappings between patterns designated by

either strings or Regexp objects, to procedures.  Optionally,
either :default or :timeout may be mapped to a procedure in
order to designate an action to take upon failure to match
any other pattern.

Returns a Hash, Regexp and boolean object.

# File lib/expectr.rb, line 369
def process_procmap(pattern_map)
  # Normalize Hash keys, allowing only Regexps and Symbols for keys.
  pattern_map = pattern_map.reduce({}) do |c,e|
    unless e[0].is_a?(Symbol) || e[0].is_a?(Regexp)
      e[0] = Regexp.new(Regexp.escape(e[0].to_s))
    end
    c.merge(e[0] => e[1])
  end

  # Separate out non-Symbol keys and build a unified Regexp.
  regex_keys = pattern_map.keys.select { |e| e.is_a?(Regexp) }
  pattern = regex_keys.reduce("") do |c,e|
    c += "|" unless c.empty?
    c + "(#{e.source})"
  end

  recoverable = regex_keys.include?(:default) || regex_keys.include?(:timeout)

  return pattern_map, pattern, recoverable
end
setup_instance() click to toggle source

Internal: Initialize instance variables to their default values.

Returns nothing.

# File lib/expectr.rb, line 282
def setup_instance
  @buffer = ''
  @discard = ''
  @thread = nil
  @out_mutex = Mutex.new
  @interact = false
end
watch_match(pattern, recoverable) click to toggle source

Internal: Watch for a match within the timeout period.

pattern - String or Regexp object containing the pattern for which to

watch.

recoverable - Boolean denoting whether a failure to find a match should be

considered fatal.

Returns a MatchData object if a match was found, or else nil. Raises Timeout::Error if no match is found and recoverable is false.

# File lib/expectr.rb, line 340
def watch_match(pattern, recoverable)
  match = nil

  Timeout::timeout(@timeout) do
    match = check_match(pattern)
  end

  @out_mutex.synchronize do
    @discard = @buffer[0..match.begin(0)-1]
    @buffer = @buffer[match.end(0)..-1]
  end

  match
rescue Timeout::Error => details
  raise(Timeout::Error, details) unless recoverable
  nil
end