ruby_expect 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ruby_expect.rb +16 -0
- data/lib/ruby_expect/expect.rb +342 -0
- data/lib/ruby_expect/procedure.rb +179 -0
- metadata +66 -0
data/lib/ruby_expect.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#####
|
2
|
+
# = LICENSE
|
3
|
+
#
|
4
|
+
# Copyright 2012 Andrew Bates Licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
#
|
16
|
+
require 'ruby_expect/expect'
|
@@ -0,0 +1,342 @@
|
|
1
|
+
#####
|
2
|
+
# = LICENSE
|
3
|
+
#
|
4
|
+
# Copyright 2012 Andrew Bates Licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'thread'
|
18
|
+
require 'ruby_expect/procedure'
|
19
|
+
require 'pty'
|
20
|
+
|
21
|
+
#####
|
22
|
+
#
|
23
|
+
#
|
24
|
+
module RubyExpect
|
25
|
+
#####
|
26
|
+
# This is the main class used to interact with IO objects An Expect object can
|
27
|
+
# be used to send and receive data on any read/write IO object.
|
28
|
+
#
|
29
|
+
class Expect
|
30
|
+
# Any data that was in the accumulator buffer before match in the last expect call
|
31
|
+
# if the last call to expect resulted in a timeout, then before is an empty string
|
32
|
+
attr_reader :before
|
33
|
+
|
34
|
+
# The exact string that matched in the last expect call
|
35
|
+
attr_reader :match
|
36
|
+
|
37
|
+
# The MatchData object from the last expect call or nil upon a timeout
|
38
|
+
attr_reader :last_match
|
39
|
+
|
40
|
+
# The accumulator buffer populated by read_loop. Only access this if you really
|
41
|
+
# know what you are doing!
|
42
|
+
attr_reader :buffer
|
43
|
+
|
44
|
+
#####
|
45
|
+
# Create a new Expect object for the given IO object
|
46
|
+
#
|
47
|
+
# There are two ways to create a new Expect object. The first is to supply
|
48
|
+
# a single IO object with a read/write mode. The second method is to supply
|
49
|
+
# a read file handle as the first argument and a write file handle as the
|
50
|
+
# second argument.
|
51
|
+
#
|
52
|
+
# +args+::
|
53
|
+
# at most 3 arguments, 1 or 2 IO objects (read/write or read + write and
|
54
|
+
# an optional options hash. The only currently supported option is :debug
|
55
|
+
# (default false) which, if enabled, will send data received on the input
|
56
|
+
# filehandle to STDOUT
|
57
|
+
#
|
58
|
+
# +block+::
|
59
|
+
# An optional block called upon initialization. See procedure
|
60
|
+
#
|
61
|
+
# == Examples
|
62
|
+
#
|
63
|
+
# # expect with a read/write filehandle
|
64
|
+
# exp = Expect.new(rwfh)
|
65
|
+
#
|
66
|
+
# # expect with separate read and write filehandles
|
67
|
+
# exp = Expect.new(rfh, wfh)
|
68
|
+
#
|
69
|
+
# # turning on debugging
|
70
|
+
# exp = Expect.new(rfh, wfh, :debug => true)
|
71
|
+
#
|
72
|
+
def initialize *args, &block
|
73
|
+
options = {}
|
74
|
+
if (args.last.is_a?(Hash))
|
75
|
+
options = args.pop
|
76
|
+
end
|
77
|
+
|
78
|
+
raise ArgumentError("First argument must be an IO object") unless (args[0].is_a?(IO))
|
79
|
+
if (args.size == 1)
|
80
|
+
@write_fh = args.shift
|
81
|
+
@read_fh = @write_fh
|
82
|
+
elsif (args.size == 2)
|
83
|
+
raise ArgumentError("Second argument must be an IO object") unless (args[1].is_a?(IO))
|
84
|
+
@write_fh = args.shift
|
85
|
+
@read_fh = args.shift
|
86
|
+
else
|
87
|
+
raise ArgumentError.new("either specify a read/write IO object, or a read IO object and a write IO object")
|
88
|
+
end
|
89
|
+
|
90
|
+
raise "Input file handle is not readable!" unless (@read_fh.stat.readable?)
|
91
|
+
raise "Output file handle is not writable!" unless (@write_fh.stat.writable?)
|
92
|
+
|
93
|
+
@buffer_sem = Mutex.new
|
94
|
+
@buffer_cv = ConditionVariable.new
|
95
|
+
@child_pid = options[:child_pid]
|
96
|
+
@debug = options[:debug] || false
|
97
|
+
@buffer = ''
|
98
|
+
@before = ''
|
99
|
+
@match = ''
|
100
|
+
@timeout = 0
|
101
|
+
|
102
|
+
read_loop # start the read thread
|
103
|
+
|
104
|
+
unless (block.nil?)
|
105
|
+
procedure(&block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
#####
|
110
|
+
# Spawn a command and interact with it
|
111
|
+
#
|
112
|
+
# +command+::
|
113
|
+
# The command to execute
|
114
|
+
#
|
115
|
+
# +block+::
|
116
|
+
# Optional block to call and run a procedure in
|
117
|
+
#
|
118
|
+
def self.spawn command, options = {}, &block
|
119
|
+
shell_in, shell_out, pid = PTY.spawn(command)
|
120
|
+
options[:child_pid] = pid
|
121
|
+
return RubyExpect::Expect.new(shell_out, shell_in, options, &block)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
#####
|
126
|
+
# Connect to a socket
|
127
|
+
#
|
128
|
+
# +command+::
|
129
|
+
# The socket or file to connect to
|
130
|
+
#
|
131
|
+
# +block+::
|
132
|
+
# Optional block to call and run a procedure in
|
133
|
+
#
|
134
|
+
def self.connect socket, options = {}, &block
|
135
|
+
require 'socket'
|
136
|
+
client = nil
|
137
|
+
if (socket.is_a?(UNIXSocket))
|
138
|
+
client = socket
|
139
|
+
else
|
140
|
+
client = UNIXSocket.new(socket)
|
141
|
+
end
|
142
|
+
return RubyExpect::Expect.new(client, options, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
#####
|
146
|
+
# Perform a series of 'expects' using the DSL defined in Procedure
|
147
|
+
#
|
148
|
+
# +block+::
|
149
|
+
# The block will be called in the context of a new Procedure object
|
150
|
+
#
|
151
|
+
# == Example
|
152
|
+
#
|
153
|
+
# exp = Expect.new(io)
|
154
|
+
# exp.procedure do
|
155
|
+
# each do
|
156
|
+
# expect /first expected line/ do
|
157
|
+
# send "some text to send"
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# expect /second expected line/ do
|
161
|
+
# send "some more text to send"
|
162
|
+
# end
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
def procedure &block
|
167
|
+
RubyExpect::Procedure.new(self, &block)
|
168
|
+
end
|
169
|
+
|
170
|
+
#####
|
171
|
+
# Set the time to wait for an expected pattern
|
172
|
+
#
|
173
|
+
# +timeout+::
|
174
|
+
# number of seconds to wait before giving up. A value of zero means wait
|
175
|
+
# forever
|
176
|
+
#
|
177
|
+
def timeout= timeout
|
178
|
+
unless (timeout.is_a?(Integer))
|
179
|
+
raise "Timeout must be an integer"
|
180
|
+
end
|
181
|
+
unless (timeout >= 0)
|
182
|
+
raise "Timeout must be greater than or equal to zero"
|
183
|
+
end
|
184
|
+
|
185
|
+
@timeout = timeout
|
186
|
+
@end_time = 0
|
187
|
+
end
|
188
|
+
|
189
|
+
####
|
190
|
+
# Get the current timeout value
|
191
|
+
#
|
192
|
+
def timeout
|
193
|
+
@timeout
|
194
|
+
end
|
195
|
+
|
196
|
+
#####
|
197
|
+
# Convenience method that will send a string followed by a newline to the
|
198
|
+
# write handle of the IO object
|
199
|
+
#
|
200
|
+
# +command+::
|
201
|
+
# String to send down the pipe
|
202
|
+
#
|
203
|
+
def send command
|
204
|
+
@write_fh.write("#{command}\n")
|
205
|
+
end
|
206
|
+
|
207
|
+
#####
|
208
|
+
# Wait until either the timeout occurs or one of the given patterns is seen
|
209
|
+
# in the input. Upon a match, the property before is assigned all input in
|
210
|
+
# the accumulator before the match, the matched string itself is assigned to
|
211
|
+
# the match property and an optional block is called
|
212
|
+
#
|
213
|
+
# The method will return the index of the matched pattern or nil if no match
|
214
|
+
# has occurred during the timeout period
|
215
|
+
#
|
216
|
+
# +patterns+::
|
217
|
+
# list of patterns to look for. These can be either literal strings or
|
218
|
+
# Regexp objects
|
219
|
+
#
|
220
|
+
# +block+::
|
221
|
+
# An optional block to be called if one of the patterns matches
|
222
|
+
#
|
223
|
+
# == Example
|
224
|
+
#
|
225
|
+
# exp = Expect.new(io)
|
226
|
+
# exp.expect('Password:') do
|
227
|
+
# send("12345")
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
def expect *patterns, &block
|
231
|
+
patterns = pattern_escape(*patterns)
|
232
|
+
@end_time = 0
|
233
|
+
if (@timeout != 0)
|
234
|
+
@end_time = Time.now + @timeout
|
235
|
+
end
|
236
|
+
|
237
|
+
@before = ''
|
238
|
+
matched_index = nil
|
239
|
+
while (@end_time == 0 || Time.now < @end_time)
|
240
|
+
return nil if (@read_fh.closed?)
|
241
|
+
@last_match = nil
|
242
|
+
@buffer_sem.synchronize do
|
243
|
+
patterns.each_index do |i|
|
244
|
+
if (match = patterns[i].match(@buffer))
|
245
|
+
@last_match = match
|
246
|
+
@before = @buffer.slice!(0...match.begin(0))
|
247
|
+
@match = @buffer.slice!(0...match.to_s.length)
|
248
|
+
matched_index = i
|
249
|
+
break
|
250
|
+
end
|
251
|
+
end
|
252
|
+
@buffer_cv.wait(@buffer_sem) if (@last_match.nil?)
|
253
|
+
end
|
254
|
+
unless (@last_match.nil?)
|
255
|
+
unless (block.nil?)
|
256
|
+
instance_eval(&block)
|
257
|
+
end
|
258
|
+
return matched_index
|
259
|
+
end
|
260
|
+
end
|
261
|
+
return nil
|
262
|
+
end
|
263
|
+
|
264
|
+
def soft_close
|
265
|
+
while (! @read_fh.closed?)
|
266
|
+
@buffer_sem.synchronize do
|
267
|
+
@buffer_cv.wait(@buffer_sem)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
@read_fh.close unless (@read_fh.closed?)
|
271
|
+
@write_fh.close unless (@write_fh.closed?)
|
272
|
+
if (@child_pid)
|
273
|
+
Process.wait(@child_pid)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
#####
|
279
|
+
# This method will convert any strings in the argument list to regular
|
280
|
+
# expressions that search for the literal string
|
281
|
+
#
|
282
|
+
# +patterns+::
|
283
|
+
# List of patterns to escape
|
284
|
+
#
|
285
|
+
def pattern_escape *patterns
|
286
|
+
escaped_patterns = []
|
287
|
+
patterns.each do |pattern|
|
288
|
+
if (pattern.is_a?(String))
|
289
|
+
pattern = Regexp.new(Regexp.escape(pattern))
|
290
|
+
elsif (! pattern.is_a?(Regexp))
|
291
|
+
raise "Don't know how to match on a #{pattern.class}"
|
292
|
+
end
|
293
|
+
escaped_patterns.push(pattern)
|
294
|
+
end
|
295
|
+
escaped_patterns
|
296
|
+
end
|
297
|
+
|
298
|
+
#####
|
299
|
+
# The read loop is an internal method that constantly waits for input to
|
300
|
+
# arrive on the IO object. When input arrives it is appended to an
|
301
|
+
# internal buffer for use by the expect method
|
302
|
+
#
|
303
|
+
def read_loop
|
304
|
+
Thread.abort_on_exception = true
|
305
|
+
Thread.new do
|
306
|
+
while (true)
|
307
|
+
begin
|
308
|
+
ready = IO.select([@read_fh], nil, nil, 1)
|
309
|
+
if (ready.nil? || ready.size == 0)
|
310
|
+
@buffer_cv.signal()
|
311
|
+
else
|
312
|
+
if (@read_fh.eof?)
|
313
|
+
@read_fh.close
|
314
|
+
@buffer_cv.signal()
|
315
|
+
break
|
316
|
+
else
|
317
|
+
input = @read_fh.readpartial(4096)
|
318
|
+
@buffer_sem.synchronize do
|
319
|
+
@buffer << input
|
320
|
+
@buffer_cv.signal()
|
321
|
+
end
|
322
|
+
if (@debug)
|
323
|
+
STDERR.print input
|
324
|
+
STDERR.flush
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
rescue EOFError => e
|
329
|
+
rescue Exception => e
|
330
|
+
unless (e.to_s == 'stream closed')
|
331
|
+
STDERR.puts "Exception in read_loop:"
|
332
|
+
STDERR.puts "#{e}"
|
333
|
+
STDERR.puts "\t#{e.backtrace.join("\n\t")}"
|
334
|
+
end
|
335
|
+
break
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
@@ -0,0 +1,179 @@
|
|
1
|
+
#####
|
2
|
+
# = LICENSE
|
3
|
+
#
|
4
|
+
# Copyright 2012 Andrew Bates Licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
#####
|
18
|
+
#
|
19
|
+
#
|
20
|
+
module RubyExpect
|
21
|
+
#####
|
22
|
+
# A pattern is a simple container to hold a string/regexp pattern and proc to
|
23
|
+
# be called upon match. This is an internal container used by the Procedure
|
24
|
+
# class
|
25
|
+
#
|
26
|
+
class Pattern
|
27
|
+
attr_reader :pattern, :block
|
28
|
+
#####
|
29
|
+
# +pattern+::
|
30
|
+
# String or Regexp objects to match on
|
31
|
+
#
|
32
|
+
# +block+::
|
33
|
+
# The block/proc to be called if a match occurs
|
34
|
+
#
|
35
|
+
def initialize pattern, &block
|
36
|
+
@pattern = pattern
|
37
|
+
@block = block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#####
|
42
|
+
# Super class for common methods for AnyMatch and EachMatch
|
43
|
+
#
|
44
|
+
class Match
|
45
|
+
#####
|
46
|
+
# +exp_object+::
|
47
|
+
# The expect object used for interaction
|
48
|
+
#
|
49
|
+
# +block+::
|
50
|
+
# The block will be called in the context of the initialized match object
|
51
|
+
#
|
52
|
+
def initialize exp_object, &block
|
53
|
+
@exp = exp_object
|
54
|
+
@patterns = []
|
55
|
+
instance_eval(&block) unless block.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
#####
|
59
|
+
# Add a pattern to be expected by the process
|
60
|
+
#
|
61
|
+
# +pattern+::
|
62
|
+
# String or Regexp to match on
|
63
|
+
#
|
64
|
+
# +block+::
|
65
|
+
# Block to be called upon a match
|
66
|
+
#
|
67
|
+
def expect pattern, &block
|
68
|
+
@patterns.push(Pattern.new(pattern, &block))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#####
|
73
|
+
# Expect any one of the specified patterns and call the matching pattern's
|
74
|
+
# block
|
75
|
+
#
|
76
|
+
class AnyMatch < Match
|
77
|
+
#####
|
78
|
+
# Procedure input data for the set of expected patterns
|
79
|
+
#
|
80
|
+
def run
|
81
|
+
retval = @exp.expect(*@patterns.collect {|p| p.pattern})
|
82
|
+
unless (retval.nil?)
|
83
|
+
@exp.instance_eval(&@patterns[retval].block) unless (@patterns[retval].block.nil?)
|
84
|
+
end
|
85
|
+
return retval
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#####
|
90
|
+
# Expect each of a set of patterns
|
91
|
+
#
|
92
|
+
class EachMatch < Match
|
93
|
+
#####
|
94
|
+
# Procedure input data for the set of expected patterns
|
95
|
+
#
|
96
|
+
def run
|
97
|
+
@patterns.each_index do |i|
|
98
|
+
retval = @exp.expect(@patterns[i].pattern, &@patterns[i].block)
|
99
|
+
return nil if (retval.nil?)
|
100
|
+
end
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
#####
|
106
|
+
# A procedure is a set of patterns to match and blocks to be called upon
|
107
|
+
# matching patterns. This is useful for building blocks of expected sequences
|
108
|
+
# of input data. An example of this could be logging into a system using SSH
|
109
|
+
#
|
110
|
+
# == Example
|
111
|
+
#
|
112
|
+
# retval = 0
|
113
|
+
# while (retval != 2)
|
114
|
+
# retval = any do
|
115
|
+
# expect /Are you sure you want to continue connecting \(yes\/no\)\?/ do
|
116
|
+
# send 'yes'
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# expect /password:\s*$/ do
|
120
|
+
# send password
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# expect /\$\s*$/ do
|
124
|
+
# send 'uptime'
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# # Expect each of the following
|
130
|
+
# each do
|
131
|
+
# expect /load\s+average:\s+\d+\.\d+,\s+\d+\.\d+,\s+\d+\.\d+/ do # expect the output of uptime
|
132
|
+
# puts last_match.to_s
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# expect /\$\s+$/ do # shell prompt
|
136
|
+
# send 'exit'
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
class Procedure
|
141
|
+
#####
|
142
|
+
# Create a new procedure to be executed by the expect object
|
143
|
+
#
|
144
|
+
# +exp_object+::
|
145
|
+
# The expect object that will execute this procedure
|
146
|
+
#
|
147
|
+
# +block+::
|
148
|
+
# The block to be called that defined the procedure
|
149
|
+
#
|
150
|
+
def initialize exp_object, &block
|
151
|
+
raise "First argument must be a RubyExpect::Expect object" unless (exp_object.is_a?(RubyExpect::Expect))
|
152
|
+
@exp = exp_object
|
153
|
+
@steps = []
|
154
|
+
instance_eval(&block) unless block.nil?
|
155
|
+
end
|
156
|
+
|
157
|
+
#####
|
158
|
+
# Add an 'any' block to the Procedure. The block will be evaluated using a
|
159
|
+
# new AnyMatch instance
|
160
|
+
#
|
161
|
+
# +block+::
|
162
|
+
# The block the specifies the patterns to expect
|
163
|
+
#
|
164
|
+
def any &block
|
165
|
+
RubyExpect::AnyMatch.new(@exp, &block).run
|
166
|
+
end
|
167
|
+
|
168
|
+
#####
|
169
|
+
# Add an 'each' block to the Procedure. The block will be evaluated using a
|
170
|
+
# new EachMatch instance
|
171
|
+
#
|
172
|
+
# +block+::
|
173
|
+
# The block that specifies the patterns to expect
|
174
|
+
#
|
175
|
+
def each &block
|
176
|
+
RubyExpect::EachMatch.new(@exp, &block).run
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_expect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: "1.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Andrew Bates
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2013-05-21 00:00:00 Z
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: Ruby implementation for send/expect interaction
|
21
|
+
email: abates@omeganetserv.com
|
22
|
+
executables: []
|
23
|
+
|
24
|
+
extensions: []
|
25
|
+
|
26
|
+
extra_rdoc_files: []
|
27
|
+
|
28
|
+
files:
|
29
|
+
- lib/ruby_expect.rb
|
30
|
+
- lib/ruby_expect/expect.rb
|
31
|
+
- lib/ruby_expect/procedure.rb
|
32
|
+
homepage: https://github.com/abates/ruby_expect
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.24
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: This is a simple expect implementation that provides interactive access to IO objects
|
65
|
+
test_files: []
|
66
|
+
|