Class: Expectr
- Inherits:
-
Object
- Object
- Expectr
- Defined in:
- lib/expectr.rb,
lib/expectr/error.rb,
lib/expectr/version.rb
Overview
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 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.
Examples
# SSH Login to another machine
exp = Expectr.new('ssh [email protected]')
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
Defined Under Namespace
Classes: ProcessError
Constant Summary collapse
- VERSION =
'1.1.0'
Instance Attribute Summary collapse
-
#buffer ⇒ Object
readonly
Public: Returns the active buffer to match against.
-
#buffer_size ⇒ Object
Public: Gets/sets the number of bytes to use for the internal buffer.
-
#constrain ⇒ Object
Public: Gets/sets whether to constrain the buffer to the buffer size.
-
#discard ⇒ Object
readonly
Public: Returns the buffer discarded by the latest call to Expectr#expect.
-
#flush_buffer ⇒ Object
Public: Gets/sets whether to flush program output to STDOUT.
-
#force_match ⇒ Object
Public: Whether to always attempt to match once on calls to Expectr#expect.
-
#pid ⇒ Object
readonly
Public: Returns the PID of the running process.
-
#timeout ⇒ Object
Public: Gets/sets the number of seconds a call to Expectr#expect may last.
Instance Method Summary collapse
-
#clear_buffer! ⇒ Object
Public: Clear output buffer.
-
#expect(pattern, recoverable = false) ⇒ Object
Public: Begin a countdown and search for a given String or Regexp in the output buffer.
-
#force_utf8(buf) ⇒ Object
Internal: Encode a String twice to force UTF-8 encoding, dropping problematic characters in the process.
-
#initialize(cmd, args = {}) ⇒ Expectr
constructor
Public: Initialize a new Expectr object.
-
#interact!(args = {}) ⇒ Object
Public: Relinquish control of the running process to the controlling terminal, acting as a pass-through for the life of the process.
-
#interact? ⇒ Boolean
Public: Report whether or not current Expectr object is in interact mode.
-
#kill!(signal = :TERM) ⇒ Object
Public: Kill the running process, raise ProcessError if the pid isn’t > 1.
-
#leave! ⇒ Object
Public: Cause the current Expectr object to drop out of interact mode.
-
#print_buffer(buf) ⇒ Object
Internal: Print buffer to STDOUT if @flush_buffer is true.
-
#puts(str = '') ⇒ Object
Public: Wraps Expectr#send, appending a newline to the end of the string.
-
#send(str) ⇒ Object
Public: Send input to the active process.
-
#winsize ⇒ Object
Public: Return the child’s window size.
Constructor Details
#initialize(cmd, args = {}) ⇒ Expectr
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 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)
:force_match - Whether to always attempt to match against the
internal buffer on a call to Expectr#expect. This
is relevant following a failed call to
Expectr#expect, which will leave the update status
set to false, preventing further matches until more
output is generated otherwise. (default: false)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/expectr.rb', line 71 def initialize(cmd, args={}) unless cmd.kind_of? String or cmd.kind_of? File raise ArgumentError, "String or File expected" end cmd = cmd.path if cmd.kind_of? File @buffer = ''.encode("UTF-8") @discard = ''.encode("UTF-8") @timeout = args[:timeout] || 30 @flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer] @buffer_size = args[:buffer_size] || 8192 @constrain = args[:constrain] || false @force_match = args[:force_match] || false @out_mutex = Mutex.new @out_update = false @interact = false @stdout,@stdin,@pid = PTY.spawn(cmd) @stdout.winsize = STDOUT.winsize Thread.new do while @pid > 0 unless select([@stdout], nil, nil, @timeout).nil? buf = ''.encode("UTF-8") begin @stdout.sysread(@buffer_size, buf) rescue Errno::EIO #Application went away. @pid = 0 break end force_utf8(buf) unless buf.valid_encoding? print_buffer(buf) @out_mutex.synchronize do @buffer << buf if @buffer.length > @buffer_size && @constrain @buffer = @buffer[-@buffer_size..-1] end @out_update = true end end end end Thread.new do Process.wait @pid @pid = 0 end end |
Instance Attribute Details
#buffer ⇒ Object (readonly)
Public: Returns the active buffer to match against
46 47 48 |
# File 'lib/expectr.rb', line 46 def buffer @buffer end |
#buffer_size ⇒ Object
Public: Gets/sets the number of bytes to use for the internal buffer
38 39 40 |
# File 'lib/expectr.rb', line 38 def buffer_size @buffer_size end |
#constrain ⇒ Object
Public: Gets/sets whether to constrain the buffer to the buffer size
40 41 42 |
# File 'lib/expectr.rb', line 40 def constrain @constrain end |
#discard ⇒ Object (readonly)
Public: Returns the buffer discarded by the latest call to Expectr#expect
48 49 50 |
# File 'lib/expectr.rb', line 48 def discard @discard end |
#flush_buffer ⇒ Object
Public: Gets/sets whether to flush program output to STDOUT
36 37 38 |
# File 'lib/expectr.rb', line 36 def flush_buffer @flush_buffer end |
#force_match ⇒ Object
Public: Whether to always attempt to match once on calls to Expectr#expect.
42 43 44 |
# File 'lib/expectr.rb', line 42 def force_match @force_match end |
#pid ⇒ Object (readonly)
Public: Returns the PID of the running process
44 45 46 |
# File 'lib/expectr.rb', line 44 def pid @pid end |
#timeout ⇒ Object
Public: Gets/sets the number of seconds a call to Expectr#expect may last
34 35 36 |
# File 'lib/expectr.rb', line 34 def timeout @timeout end |
Instance Method Details
#clear_buffer! ⇒ Object
Public: Clear output buffer
Returns nothing.
297 298 299 300 301 302 |
# File 'lib/expectr.rb', line 297 def clear_buffer! @out_mutex.synchronize do @buffer = ''.encode("UTF-8") @out_update = false end end |
#expect(pattern, recoverable = false) ⇒ Object
Public: Begin a countdown and search for a given String or Regexp in the output buffer.
pattern - String or Regexp representing what we want to find 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
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
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/expectr.rb', line 257 def expect(pattern, recoverable = false) match = nil @out_update = true if @force_match case pattern when String pattern = Regexp.new(Regexp.quote(pattern)) when Regexp else raise TypeError, "Pattern class should be String or Regexp" end begin Timeout::timeout(@timeout) do while match.nil? if @out_update @out_mutex.synchronize do match = pattern.match @buffer @out_update = false end end sleep 0.1 end end @out_mutex.synchronize do @discard = @buffer[0..match.begin(0)-1] @buffer = @buffer[match.end(0)..-1] @out_update = true end rescue Timeout::Error => details raise details unless recoverable end block_given? ? yield(match) : match end |
#force_utf8(buf) ⇒ Object
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.
327 328 329 |
# File 'lib/expectr.rb', line 327 def force_utf8(buf) buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil) end |
#interact!(args = {}) ⇒ Object
Public: Relinquish control of the running process to the controlling terminal, acting as a pass-through for the life of the process. SIGINT will be caught and sent to the application as “C-c”.
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
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/expectr.rb', line 137 def interact!(args = {}) raise ProcessError if @interact blocking = args[:blocking] || false @flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer] @interact = true # Save our old tty settings and set up our new environment old_tty = `stty -g` `stty -icanon min 1 time 0 -echo` # SIGINT should be sent along to the process. old_int_trap = trap 'INT' do send "\C-c" end # SIGTSTP should be sent along to the process as well. old_tstp_trap = trap 'TSTP' do send "\C-z" end # SIGWINCH should trigger an update to the child process old_winch_trap = trap 'WINCH' do @stdout.winsize = STDOUT.winsize end interact = Thread.new do input = ''.encode("UTF-8") while @pid > 0 && @interact if select([STDIN], nil, nil, 1) c = STDIN.getc.chr send c unless c.nil? end end trap 'INT', old_int_trap trap 'TSTP', old_tstp_trap trap 'WINCH', old_winch_trap `stty #{old_tty}` @interact = false end blocking ? interact.join : interact end |
#interact? ⇒ Boolean
Public: Report whether or not current Expectr object is in interact mode
Returns true or false
185 186 187 |
# File 'lib/expectr.rb', line 185 def interact? @interact end |
#kill!(signal = :TERM) ⇒ Object
Public: Kill the running process, raise ProcessError if the pid isn’t > 1
signal - Symbol, String, or Fixnum representing the signal to send to the
running process. (default: :TERM)
Returns true if the process was successfully killed, false otherwise
202 203 204 205 |
# File 'lib/expectr.rb', line 202 def kill!(signal=:TERM) raise ProcessError unless @pid > 0 (Process::kill(signal.to_sym, @pid) == 1) end |
#leave! ⇒ Object
Public: Cause the current Expectr object to drop out of interact mode
Returns nothing.
192 193 194 |
# File 'lib/expectr.rb', line 192 def leave! @interact=false end |
#print_buffer(buf) ⇒ Object
Internal: Print buffer to STDOUT if @flush_buffer is true
buf - String to be printed to STDOUT
Returns nothing.
316 317 318 319 |
# File 'lib/expectr.rb', line 316 def print_buffer(buf) print buf if @flush_buffer STDOUT.flush unless STDOUT.sync end |
#puts(str = '') ⇒ Object
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.
227 228 229 |
# File 'lib/expectr.rb', line 227 def puts(str = '') send str + "\n" end |
#send(str) ⇒ Object
Public: Send input to the active process
str - String to be sent to the active process
Returns nothing. Raises Expectr::ProcessError if the process isn’t running
213 214 215 216 217 218 219 220 |
# File 'lib/expectr.rb', line 213 def send(str) begin @stdin.syswrite str rescue Errno::EIO #Application went away. @pid = 0 end raise Expectr::ProcessError unless @pid > 0 end |
#winsize ⇒ Object
Public: Return the child’s window size.
Returns a two-element array (same as IO#winsize).
307 308 309 |
# File 'lib/expectr.rb', line 307 def winsize @stdout.winsize end |