ruby_expect 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+