lib/expectr.rb in expectr-2.0.0 vs lib/expectr.rb in expectr-2.0.1
- old
+ new
@@ -11,14 +11,13 @@
require 'expectr/lambda'
# Public: Expectr is an API to the functionality of Expect (see
# http://expect.nist.gov) implemented in ruby.
#
-# Expectr contrasts with Ruby's built-in Expect class by avoiding tying in
-# with the IO class, instead creating a new object entirely to allow for more
-# grainular control over the execution and display of the program being
-# run.
+# 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')
@@ -55,89 +54,96 @@
attr_reader :discard
# Public: Initialize a new Expectr object.
# Spawns a sub-process and attaches to STDIN and STDOUT for the new process.
#
- # cmd - A String or File referencing the application to launch (default: '')
- # 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 (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 (default: false)
- # :interface - Interface Object to use when instantiating the new
- # Expectr object. (default: Child)
- def initialize(cmd = '', args = {})
+ # 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)
+ def initialize(cmd_args = '', args = {})
setup_instance
parse_options(args)
- case args[:interface]
- when :lambda
- interface = call_lambda_interface(args)
- when :adopt
- interface = call_adopt_interface(args)
- else
- interface = call_child_interface(cmd)
- end
+ cmd_args = { cmd: cmd_args } unless cmd_args.is_a?(Hash)
+ args.merge!(cmd_args)
- interface.init_instance.each do |spec|
- ->(name, func) { define_singleton_method(name, func.call) }.call(*spec)
+ 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: Relinquish control of the running process to the controlling
+ # 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:
- # {}):
+ # 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
+ # Returns the interaction Thread, calling #join on it if :blocking is true.
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
- # Public: Report whether or not current Expectr object is in interact mode
+ # Public: Report whether or not current Expectr object is in interact mode.
#
- # Returns true or false
+ # Returns a boolean.
def interact?
@interact
end
- # Public: Cause the current Expectr object to drop out of interact mode
+ # Public: Cause the current Expectr object to leave interact mode.
#
# Returns nothing.
def leave!
@interact=false
end
- # Public: Wraps Expectr#send, appending a newline to the end of the string
+ # Public: Wraps Expectr#send, appending a newline to the end of the string.
#
- # str - String to be sent to the active process (default: '')
+ # str - String to be sent to the active process. (default: '')
#
# Returns nothing.
def puts(str = '')
send str + "\n"
end
# Public: Begin a countdown and search for a given String or Regexp in the
- # output buffer.
+ # 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.
@@ -161,11 +167,11 @@
#
# 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
def expect(pattern, recoverable = false)
@@ -194,11 +200,11 @@
# exp.expect_procmap({
# "option 1" => -> { puts "action 1" },
# /option 2/ => -> { puts "action 2" },
# :default => -> { puts "default" }
# })
- #
+ #
# Calls the procedure associated with the pattern provided.
def expect_procmap(pattern_map)
pattern_map, pattern, recoverable = process_procmap(pattern_map)
match = nil
@@ -213,44 +219,44 @@
pattern_map[:default].call unless pattern_map[:default].nil?
pattern_map[:timeout].call unless pattern_map[:timeout].nil?
nil
end
- # Public: Clear output buffer
+ # Public: Clear output buffer.
#
# Returns nothing.
def clear_buffer!
@out_mutex.synchronize do
@buffer.clear
end
end
private
- # Internal: Print buffer to $stdout if @flush_buffer is true
+ # Internal: Print buffer to $stdout if program output is expected to be
+ # echoed.
#
- # buf - String to be printed to $stdout
+ # buf - String to be printed to $stdout.
#
# Returns nothing.
def print_buffer(buf)
$stdout.print buf if @flush_buffer
$stdout.flush unless $stdout.sync
end
- # 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.
- def force_utf8(buf)
+ # 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.
+ def force_utf8(buf)
return buf if buf.valid_encoding?
- buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)
- end
+ buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)
+ end
- # Internal: Determine values of instance options and set instance variables
- # appropriately, allowing for default values where nothing is passed.
+ # 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
@@ -281,11 +287,11 @@
@interact = false
end
# 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.
def process_output(buf)
force_utf8(buf)
print_buffer(buf)
@@ -300,12 +306,11 @@
# 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 object containing the pattern for which to
- # watch.
+ # pattern - String or Regexp containing the pattern for which to watch.
#
# Returns a MatchData object containing the match found.
def check_match(pattern)
match = nil
@thread = Thread.current
@@ -318,66 +323,12 @@
match
ensure
@thread = nil
end
- # Internal: Call the Child Interface to instantiate the Expectr object.
- #
- # cmd - String or File object referencing the command to be run.
- #
- # Returns the Interface object.
- def call_child_interface(cmd)
- interface = Expectr::Child.new(cmd)
- @stdin = interface.stdin
- @stdout = interface.stdout
- @pid = interface.pid
-
- Thread.new do
- Process.wait @pid
- @pid = 0
- end
-
- interface
- end
-
- # Internal: Call the Lambda Interface to instantiate the Expectr object.
- #
- # args - Arguments hash passed per #initialize.
- #
- # Returns the Interface object.
- def call_lambda_interface(args)
- interface = Expectr::Lambda.new(args[:reader], args[:writer])
- @pid = -1
- @reader = interface.reader
- @writer = interface.writer
-
- interface
- end
-
- # Internal: Call the Adopt Interface to instantiate the Expectr object.
- #
- # args - Arguments hash passed per #initialize.
- #
- # Returns the Interface object.
- def call_adopt_interface(args)
- interface = Expectr::Adopt.new(args[:stdin], args[:stdout])
- @stdin = interface.stdin
- @stdout = interface.stdout
- @pid = args[:pid] || 1
-
- if @pid > 0
- Thread.new do
- Process.wait @pid
- @pid = 0
- end
- end
-
- interface
- end
-
# 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.
#
@@ -402,26 +353,34 @@
end
# 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 timeout.
+ # order to designate an action to take upon failure to match
+ # any other pattern.
#
# Returns a Hash, Regexp and boolean object.
def process_procmap(pattern_map)
+ # Normalize Hash keys, allowing only Regexps and Symbols for keys.
pattern_map = pattern_map.reduce({}) do |c,e|
- c.merge((e[0].is_a?(Symbol) ? e[0] : Regexp.new(e[0].to_s)) => e[1])
+ 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
- pattern = pattern_map.keys.reduce("") do |c,e|
- e.is_a?(Regexp) ? c + "(#{e.source})|" : c
+
+ # 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
- pattern = Regexp.new(pattern.gsub(/\|$/, ''))
- recoverable = pattern_map.keys.include?(:default)
- recoverable ||= pattern_map.keys.include?(:timeout)
+
+ recoverable = regex_keys.include?(:default) || regex_keys.include?(:timeout)
return pattern_map, pattern, recoverable
end
end