ruby_expect 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+