├── .rspec ├── lib ├── ruby_expect │ ├── version.rb │ ├── errors.rb │ ├── procedure.rb │ └── expect.rb └── ruby_expect.rb ├── .travis.yml ├── Gemfile ├── .gitignore ├── Rakefile ├── bin ├── setup └── console ├── spec ├── spec_helper.rb ├── ruby_expect_spec.rb └── ruby_expect │ ├── procedure_spec.rb │ └── expect_spec.rb ├── NOTICE ├── ruby_expect.gemspec ├── README.md └── LICENSE /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/ruby_expect/version.rb: -------------------------------------------------------------------------------- 1 | module RubyExpect 2 | VERSION = "1.7.4" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | before_install: gem install bundler -v 1.10.6 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ruby_expect.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'ruby_expect' 6 | -------------------------------------------------------------------------------- /lib/ruby_expect/errors.rb: -------------------------------------------------------------------------------- 1 | module RubyExpect 2 | ##### 3 | # Raised when attempt is made to interact with a closed filehandle 4 | # 5 | class ClosedError < StandardError 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ruby_expect" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Andrew Bates 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | 14 | -------------------------------------------------------------------------------- /lib/ruby_expect.rb: -------------------------------------------------------------------------------- 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/version" 17 | require 'ruby_expect/expect' 18 | require 'ruby_expect/errors' 19 | -------------------------------------------------------------------------------- /spec/ruby_expect_spec.rb: -------------------------------------------------------------------------------- 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 'spec_helper' 18 | 19 | describe RubyExpect do 20 | it 'has a version number' do 21 | expect(RubyExpect::VERSION).not_to be nil 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /ruby_expect.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: ruby_expect 1.7.4 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "ruby_expect" 6 | s.version = "1.7.5" 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 9 | s.require_paths = ["lib"] 10 | s.authors = ["Andrew Bates"] 11 | s.bindir = "exe" 12 | s.date = "2016-01-06" 13 | s.description = "Ruby implementation for send/expect interaction" 14 | s.email = ["abates@omeganetserv.com"] 15 | s.files = [".gitignore", ".rspec", ".travis.yml", "Gemfile", "LICENSE", "NOTICE", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/ruby_expect.rb", "lib/ruby_expect/errors.rb", "lib/ruby_expect/expect.rb", "lib/ruby_expect/procedure.rb", "lib/ruby_expect/version.rb", "ruby_expect.gemspec"] 16 | s.homepage = "https://github.com/abates/ruby_expect" 17 | s.rubygems_version = "2.4.8" 18 | s.summary = "This is a simple expect implementation that provides interactive access to IO objects" 19 | 20 | if s.respond_to? :specification_version then 21 | s.specification_version = 4 22 | 23 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 24 | s.add_development_dependency(%q, ["~> 1.10"]) 25 | s.add_development_dependency(%q, [">= 12.3.3"]) 26 | s.add_development_dependency(%q, [">= 0"]) 27 | s.add_development_dependency(%q, [">= 0"]) 28 | else 29 | s.add_dependency(%q, ["~> 1.10"]) 30 | s.add_development_dependency(%q, [">= 12.3.3"]) 31 | s.add_dependency(%q, [">= 0"]) 32 | s.add_dependency(%q, [">= 0"]) 33 | end 34 | else 35 | s.add_dependency(%q, ["~> 1.10"]) 36 | s.add_development_dependency(%q, [">= 12.3.3"]) 37 | s.add_dependency(%q, [">= 0"]) 38 | s.add_dependency(%q, [">= 0"]) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/ruby_expect/procedure_spec.rb: -------------------------------------------------------------------------------- 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 'spec_helper' 18 | require 'socket' 19 | 20 | describe RubyExpect::Procedure do 21 | before :each do 22 | (s1, s2) = UNIXSocket.socketpair 23 | s2 << "line1\n" 24 | s2 << "line2\n" 25 | s2 << "line3\n" 26 | s2.flush 27 | 28 | match1 = false 29 | match2 = false 30 | 31 | @exp = RubyExpect::Expect.new(s1) 32 | end 33 | 34 | it 'provides a way to expect a series of patterns' do 35 | match1 = false 36 | match2 = false 37 | @exp.procedure do 38 | each do 39 | expect /line2/ do 40 | match1 = true 41 | end 42 | 43 | expect /line3/ do 44 | match2 = true 45 | end 46 | end 47 | end 48 | expect(match1).to be(true) 49 | expect(match2).to be(true) 50 | end 51 | 52 | it 'provides a way to match any pattern in a set of patterns' do 53 | match1 = false 54 | match2 = false 55 | match3 = false 56 | match4 = false 57 | this = self 58 | @exp.procedure do 59 | any do 60 | expect /line2/ do 61 | match1 = true 62 | end 63 | expect /line22/ do 64 | match2 = true 65 | end 66 | end 67 | 68 | retval = any do 69 | expect /line33/ do 70 | match3 = true 71 | end 72 | expect /line3/ do 73 | match4 = true 74 | end 75 | end 76 | this.expect(retval).to this.be(1) 77 | end 78 | 79 | expect(match1).to be(true) 80 | expect(match2).to be(false) 81 | expect(match3).to be(false) 82 | expect(match4).to be(true) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RubyExpect 2 | ========== 3 | [![Gem Version](https://badge.fury.io/rb/ruby_expect.svg)](https://badge.fury.io/rb/ruby_expect) 4 | [![Build Status](https://travis-ci.org/abates/ruby_expect.svg?branch=develop)](https://travis-ci.org/abates/ruby_expect) 5 | 6 | Introduction 7 | ------------ 8 | 9 | This is a simple expect API written in pure ruby. Part of this library includes a simple procedure DSL 10 | for creating sequences of expect/send behavior. 11 | 12 | Examples 13 | -------- 14 | 15 | ### SSH to a host and run a command 16 | This example will use the system ssh binary and connect to a host. Upon connecting it will 17 | execute a command and wait for the response then exit. The response from the command will be 18 | parsed and printed to the screen. 19 | 20 | ```ruby 21 | #!/usr/bin/ruby 22 | 23 | require 'ruby_expect' 24 | 25 | username = 'username' 26 | password = 'password' 27 | hostname = 'hostname' 28 | 29 | exp = RubyExpect::Expect.spawn("/usr/bin/ssh #{username}@#{hostname}") 30 | 31 | exp.procedure do 32 | retval = 0 33 | while (retval != 2) 34 | retval = any do 35 | expect /Are you sure you want to continue connecting \(yes\/no\)\?/ do 36 | send 'yes' 37 | end 38 | 39 | expect /password:\s*$/ do 40 | send password 41 | end 42 | 43 | expect /\$\s*$/ do 44 | send 'uptime' 45 | end 46 | end 47 | end 48 | 49 | # Expect each of the following 50 | each do 51 | expect /load\s+average:\s+\d+\.\d+,\s+\d+\.\d+,\s+\d+\.\d+/ do # expect the output of uptime 52 | puts last_match.to_s 53 | end 54 | 55 | expect /\$\s+$/ do # shell prompt 56 | send 'exit' 57 | end 58 | end 59 | end 60 | ``` 61 | 62 | ### Interact with a local script 63 | This example runs a script and interacts with it 64 | ```ruby 65 | #!/usr/bin/ruby 66 | 67 | require 'ruby_expect' 68 | 69 | root_password = 'root_password' 70 | new_root_password = 'new_password' 71 | 72 | exp = RubyExpect::Expect.spawn('mysql_secure_installation', :debug => true) 73 | 74 | exp.procedure do 75 | each do 76 | expect "Enter current password for root (enter for none):" do 77 | send root_password 78 | end 79 | 80 | expect "Change the root password? [Y/n]" do 81 | send "y" 82 | end 83 | 84 | expect "New password:" do 85 | send new_root_password 86 | end 87 | 88 | expect "Re-enter new password:" do 89 | send new_root_password 90 | end 91 | 92 | expect "Remove anonymous users?" do 93 | send "y" 94 | end 95 | 96 | expect "Disallow root login remotely?" do 97 | send "y" 98 | end 99 | 100 | expect "Remove test database and access to it?" do 101 | send "y" 102 | end 103 | 104 | expect "Reload privilege tables now?" do 105 | send "y" 106 | end 107 | end 108 | end 109 | 110 | puts "Ended expect script." 111 | ``` 112 | 113 | 114 | ### SSH to a host and interact 115 | This example will spawn ssh and login to the host. Once logged in, the interact 116 | method is called which returns control to the user and allows them to interact 117 | directly with the remote system 118 | 119 | ```ruby 120 | #!/usr/bin/ruby 121 | 122 | require 'ruby_expect' 123 | 124 | username = 'username' 125 | password = 'password' 126 | hostname = 'hostname' 127 | 128 | exp = RubyExpect::Expect.spawn("/usr/bin/ssh #{username}@#{hostname}") 129 | exp.procedure do 130 | retval = 0 131 | while (retval != 2) 132 | retval = any do 133 | # Accept the key if it isn't already known 134 | expect /Are you sure you want to continue connecting \(yes\/no\)\?/ do 135 | send 'yes' 136 | end 137 | 138 | # Send the password at the prompt 139 | expect /password:\s*$/ do 140 | send password 141 | end 142 | 143 | # Expect the shell prompt 144 | expect /\$\s*$/ do 145 | send "" 146 | end 147 | end 148 | end 149 | end 150 | 151 | # Pass control back to the user 152 | exp.interact 153 | ``` 154 | -------------------------------------------------------------------------------- /lib/ruby_expect/procedure.rb: -------------------------------------------------------------------------------- 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 180 | -------------------------------------------------------------------------------- /spec/ruby_expect/expect_spec.rb: -------------------------------------------------------------------------------- 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 'spec_helper' 18 | require 'socket' 19 | require 'stringio' 20 | require 'tempfile' 21 | 22 | describe RubyExpect::Expect do 23 | before :each do 24 | (@s1, @s2) = UNIXSocket.socketpair 25 | @s2 << "line1\n" 26 | @s2 << "line2\n" 27 | @s2 << "line3\n" 28 | @s2.flush 29 | end 30 | 31 | it 'should return when the expected strings have been encountered in the stream' do 32 | exp = RubyExpect::Expect.new(@s1) 33 | expect(exp.expect("line2\n")).to eq(0) 34 | end 35 | 36 | it 'provides access to the data before the expected string' do 37 | exp = RubyExpect::Expect.new(@s1) 38 | exp.expect("line2\n") 39 | expect(exp.before).to eq("line1\n") 40 | end 41 | 42 | it 'provides access to the matched string' do 43 | exp = RubyExpect::Expect.new(@s1) 44 | exp.expect("line2\n") 45 | expect(exp.match).to eq("line2\n") 46 | end 47 | 48 | it 'returns the index of the first matching pattern when multiple patterns are given' do 49 | exp = RubyExpect::Expect.new(@s1) 50 | expect(exp.expect("line2\n", "line3\n")).to eq(0) 51 | expect(exp.expect("line2\n", "line3\n")).to eq(1) 52 | end 53 | 54 | it 'calls the block given when an expected pattern is matched' do 55 | exp = RubyExpect::Expect.new(@s1) 56 | proc_called = false 57 | exp.expect("line2\n") do 58 | proc_called = true 59 | end 60 | 61 | expect(proc_called).to be(true) 62 | end 63 | 64 | it 'returns nil if it times out while expecting a pattern' do 65 | exp = RubyExpect::Expect.new(@s1) 66 | exp.timeout = 1 67 | expect(exp.expect('foobar')).to eq(nil) 68 | end 69 | 70 | it 'provides the ability for the user to directly interact with the IO stream' do 71 | exp = RubyExpect::Expect.new(@s1) 72 | exp.expect("line3\n") 73 | (old_stdin, old_stdout) = [$stdin, $stdout] 74 | (stdio1, stdio2) = UNIXSocket.socketpair 75 | $stdin = stdio2 76 | $stdout = stdio2 77 | 78 | Thread.new do 79 | exp.interact 80 | end 81 | 82 | stdio1 << "First Line\n" 83 | stdio1.flush 84 | 85 | expect(@s2.gets).to eq("First Line\n") 86 | @s2 << "First Response\n" 87 | @s2.flush 88 | expect(stdio1.gets).to eq("First Response\n") 89 | @s2.close 90 | 91 | $stdin = old_stdin 92 | $stdout = old_stdout 93 | end 94 | 95 | it 'will write data out to the file handle when calling the send method' do 96 | exp = RubyExpect::Expect.new(@s1) 97 | (s1, s2) = UNIXSocket.socketpair 98 | exp = RubyExpect::Expect.new(s1) 99 | exp.send("a line of text") 100 | line = s2.gets 101 | expect(line).to eq("a line of text\n") 102 | end 103 | 104 | it 'will wait for the filehandle to be closed before exiting' do 105 | socket_file = Dir::Tmpname.make_tmpname('ruby_expect_test_socket', nil) 106 | File.unlink(socket_file) if (File.exists?(socket_file)) 107 | 108 | server = UNIXServer.new(socket_file) 109 | fork do 110 | socket = server.accept 111 | line = '' 112 | begin 113 | while (line = socket.gets) 114 | line.strip! 115 | case line 116 | when 'list' 117 | socket.print "item1\nitem2\nitem3\nitem4\n" 118 | when /set (\w+)=(\w+)/ 119 | socket.print "New value for #{$1} is #{$2}\n" 120 | when 'exit' 121 | sleep 2 122 | socket.print "Exiting\n" 123 | break 124 | end 125 | end 126 | ensure 127 | socket.close 128 | end 129 | end 130 | exp = RubyExpect::Expect.connect(socket_file) 131 | 132 | exp.send("list") 133 | exp.procedure do 134 | each do 135 | expect /item2$/ do 136 | send "set item2=newValue" 137 | end 138 | expect /item2 is newValue$/ do 139 | send 'exit' 140 | end 141 | end 142 | end 143 | exp.soft_close 144 | buffer = exp.buffer 145 | File.unlink(socket_file) if (File.exists?(socket_file)) 146 | expect(buffer).to match(/Exiting$/) 147 | end 148 | 149 | it 'shouldn\'t interfere with processes after spawned process has closed' do 150 | exp = RubyExpect::Expect.spawn('sleep 2') 151 | expect(exp.soft_close.exitstatus).to eq(0) 152 | `ls` 153 | end 154 | 155 | it 'should return the spawned process status after closing' do 156 | exp = RubyExpect::Expect.spawn('ls foo') 157 | expect(exp.soft_close.exitstatus).to_not eq(0) 158 | end 159 | 160 | it 'should raise an error if expect is called after the read handle is closed' do 161 | exp = RubyExpect::Expect.new(@s1) 162 | @s1.close 163 | expect { 164 | exp.expect("line2\n") 165 | }.to raise_error(RubyExpect::ClosedError) 166 | end 167 | 168 | it 'should use an optional logger to receive data sent and received on the IO filehandle' do 169 | logger = double 170 | allow(logger).to receive(:debug?).and_return(true) 171 | allow(logger).to receive(:info?).and_return(true) 172 | expect(logger).to receive(:debug).with("Expecting: [\"line1\"]") 173 | expect(logger).to receive(:info).with(" Received: line1") 174 | expect(logger).to receive(:info).with(" Received: line2") 175 | expect(logger).to receive(:info).with(" Received: line3") 176 | expect(logger).to receive(:debug).with(" Matched: line1") 177 | exp = RubyExpect::Expect.new(@s1, logger: logger) 178 | exp.expect("line1") 179 | end 180 | end 181 | 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Andrew Bates 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/ruby_expect/expect.rb: -------------------------------------------------------------------------------- 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 | require 'io/console' 21 | require 'logger' 22 | 23 | ##### 24 | # 25 | # 26 | module RubyExpect 27 | ##### 28 | # This is the main class used to interact with IO objects An Expect object can 29 | # be used to send and receive data on any read/write IO object. 30 | # 31 | class Expect 32 | # Any data that was in the accumulator buffer before match in the last expect call 33 | # if the last call to expect resulted in a timeout, then before is an empty string 34 | attr_reader :before 35 | 36 | # The exact string that matched in the last expect call 37 | attr_reader :match 38 | 39 | # The MatchData object from the last expect call or nil upon a timeout 40 | attr_reader :last_match 41 | 42 | # The accumulator buffer populated by read_loop. Only access this if you really 43 | # know what you are doing! 44 | attr_reader :buffer 45 | 46 | # Any supplied logger will be used both for errors and warnings as well 47 | # as debug information (I/O when sending and expecting) 48 | attr_accessor :logger 49 | 50 | ##### 51 | # Create a new Expect object for the given IO object 52 | # 53 | # There are two ways to create a new Expect object. The first is to supply 54 | # a single IO object with a read/write mode. The second method is to supply 55 | # a read file handle as the first argument and a write file handle as the 56 | # second argument. 57 | # 58 | # +args+:: 59 | # at most 3 arguments, 1 or 2 IO objects (read/write or read + write and 60 | # an optional options hash. The only currently supported option is :debug 61 | # (default false) which, if enabled, will send data received on the input 62 | # filehandle to STDOUT 63 | # 64 | # +block+:: 65 | # An optional block called upon initialization. See procedure 66 | # 67 | # == Examples 68 | # 69 | # # expect with a read/write filehandle 70 | # exp = Expect.new(rwfh) 71 | # 72 | # # expect with separate read and write filehandles 73 | # exp = Expect.new(rfh, wfh) 74 | # 75 | # # turning on debugging 76 | # exp = Expect.new(rfh, wfh, :debug => true) 77 | # 78 | def initialize *args, &block 79 | options = {} 80 | if (args.last.is_a?(Hash)) 81 | options = args.pop 82 | end 83 | 84 | raise ArgumentError("First argument must be an IO object") unless (args[0].is_a?(IO)) 85 | if (args.size == 1) 86 | @write_fh = args.shift 87 | @read_fh = @write_fh 88 | elsif (args.size == 2) 89 | raise ArgumentError("Second argument must be an IO object") unless (args[1].is_a?(IO)) 90 | @write_fh = args.shift 91 | @read_fh = args.shift 92 | else 93 | raise ArgumentError.new("either specify a read/write IO object, or a read IO object and a write IO object") 94 | end 95 | 96 | raise "Input file handle is not readable!" unless (@read_fh.stat.readable?) 97 | raise "Output file handle is not writable!" unless (@write_fh.stat.writable?) 98 | 99 | @child_pid = options[:child_pid] 100 | @debug = options[:debug] || false 101 | @logger = options[:logger] 102 | if @logger.nil? 103 | @logger = Logger.new(STDERR) 104 | @logger.level = Logger::FATAL 105 | end 106 | 107 | @buffer = '' 108 | @log_buffer = '' 109 | @before = '' 110 | @match = '' 111 | @timeout = 0 112 | 113 | unless (block.nil?) 114 | procedure(&block) 115 | end 116 | end 117 | 118 | ##### 119 | # Spawn a command and interact with it 120 | # 121 | # +command+:: 122 | # The command to execute 123 | # 124 | # +block+:: 125 | # Optional block to call and run a procedure in 126 | # 127 | def self.spawn command, options = {}, &block 128 | shell_in, shell_out, pid = PTY.spawn(command) 129 | options[:child_pid] = pid 130 | return RubyExpect::Expect.new(shell_out, shell_in, options, &block) 131 | end 132 | 133 | 134 | ##### 135 | # Connect to a socket 136 | # 137 | # +command+:: 138 | # The socket or file to connect to 139 | # 140 | # +block+:: 141 | # Optional block to call and run a procedure in 142 | # 143 | def self.connect socket, options = {}, &block 144 | require 'socket' 145 | client = nil 146 | if (socket.is_a?(UNIXSocket)) 147 | client = socket 148 | else 149 | client = UNIXSocket.new(socket) 150 | end 151 | return RubyExpect::Expect.new(client, options, &block) 152 | end 153 | 154 | ##### 155 | # Set debug in order to see the output being read from the spawned process 156 | def debug= debug 157 | warn "`debug` is deprecated. Use a logger instead" 158 | if debug 159 | @logger.level = Logger::DEBUG 160 | else 161 | @logger.level = -1 162 | end 163 | end 164 | 165 | def debug 166 | warn "`debug` is deprecated. Use a logger instead" 167 | @logger.debug? 168 | end 169 | 170 | def debug? 171 | warn "`debug` is deprecated. Use a logger instead" 172 | @logger.debug? 173 | end 174 | 175 | ##### 176 | # Perform a series of 'expects' using the DSL defined in Procedure 177 | # 178 | # +block+:: 179 | # The block will be called in the context of a new Procedure object 180 | # 181 | # == Example 182 | # 183 | # exp = Expect.new(io) 184 | # exp.procedure do 185 | # each do 186 | # expect /first expected line/ do 187 | # send "some text to send" 188 | # end 189 | # 190 | # expect /second expected line/ do 191 | # send "some more text to send" 192 | # end 193 | # end 194 | # end 195 | # 196 | def procedure &block 197 | RubyExpect::Procedure.new(self, &block) 198 | end 199 | 200 | ##### 201 | # Set the time to wait for an expected pattern 202 | # 203 | # +timeout+:: 204 | # number of seconds to wait before giving up. A value of zero means wait 205 | # forever 206 | # 207 | def timeout= timeout 208 | unless (timeout.is_a?(Integer)) 209 | raise "Timeout must be an integer" 210 | end 211 | unless (timeout >= 0) 212 | raise "Timeout must be greater than or equal to zero" 213 | end 214 | 215 | @timeout = timeout 216 | @end_time = 0 217 | end 218 | 219 | #### 220 | # Get the current timeout value 221 | # 222 | def timeout 223 | @timeout 224 | end 225 | 226 | ##### 227 | # Convenience method that will send a string followed by a newline to the 228 | # write handle of the IO object 229 | # 230 | # +command+:: 231 | # String to send down the pipe 232 | # 233 | def send command 234 | @write_fh.write("#{command}\n") 235 | end 236 | 237 | ##### 238 | # Wait until either the timeout occurs or one of the given patterns is seen 239 | # in the input. Upon a match, the property before is assigned all input in 240 | # the accumulator before the match, the matched string itself is assigned to 241 | # the match property and an optional block is called 242 | # 243 | # The method will return the index of the matched pattern or nil if no match 244 | # has occurred during the timeout period 245 | # 246 | # +patterns+:: 247 | # list of patterns to look for. These can be either literal strings or 248 | # Regexp objects 249 | # 250 | # +block+:: 251 | # An optional block to be called if one of the patterns matches 252 | # 253 | # == Example 254 | # 255 | # exp = Expect.new(io) 256 | # exp.expect('Password:') do 257 | # send("12345") 258 | # end 259 | # 260 | def expect *patterns, &block 261 | @logger.debug("Expecting: #{patterns.inspect}") if @logger.debug? 262 | patterns = pattern_escape(*patterns) 263 | @end_time = 0 264 | if (@timeout != 0) 265 | @end_time = Time.now + @timeout 266 | end 267 | 268 | @before = '' 269 | matched_index = nil 270 | while (@end_time == 0 || Time.now < @end_time) 271 | raise ClosedError.new("Read filehandle is closed") if (@read_fh.closed?) 272 | break unless (read_proc) 273 | @last_match = nil 274 | patterns.each_index do |i| 275 | if (match = patterns[i].match(@buffer)) 276 | log_buffer(true) 277 | @logger.debug(" Matched: #{match}") if @logger.debug? 278 | @last_match = match 279 | @before = @buffer.slice!(0...match.begin(0)) 280 | @match = @buffer.slice!(0...match.to_s.length) 281 | matched_index = i 282 | break 283 | end 284 | end 285 | unless (@last_match.nil?) 286 | unless (block.nil?) 287 | instance_eval(&block) 288 | end 289 | return matched_index 290 | end 291 | end 292 | @logger.debug("Timeout") 293 | return nil 294 | end 295 | 296 | ##### 297 | # Wait for the process to complete or the read handle to be closed 298 | # and then clean everything up. This method call will block until 299 | # the spawned process or connected filehandle/socket is closed 300 | # 301 | def soft_close 302 | while (! @read_fh.closed?) 303 | read_proc 304 | end 305 | @read_fh.close unless (@read_fh.closed?) 306 | @write_fh.close unless (@write_fh.closed?) 307 | if (@child_pid) 308 | Process.wait(@child_pid) 309 | return $? 310 | end 311 | return true 312 | end 313 | 314 | ### 315 | # Provides the ability to hand control back to the user and 316 | # allow them to interact with the spawned process. 317 | def interact 318 | if ($stdin.tty?) 319 | $stdin.raw do |stdin| 320 | interact_loop(stdin) 321 | end 322 | else 323 | interact_loop($stdin) 324 | end 325 | end 326 | 327 | private 328 | def log_buffer incomplete_lines=false 329 | return unless @logger.info? 330 | if @log_buffer =~ /[\r\n]/ 331 | lines = @log_buffer.split(/[\r\n]+/, -1) 332 | @log_buffer = lines.pop 333 | lines.each do |line| 334 | @logger.info(" Received: #{line.scan(/[[:print:]]/).join}") 335 | end 336 | end 337 | 338 | if incomplete_lines and @log_buffer !~ /^\s*$/ 339 | @logger.info(" Received: #{@log_buffer.scan(/[[:print:]]/).join}") 340 | @log_buffer = '' 341 | end 342 | end 343 | 344 | def interact_loop stdin 345 | done = false 346 | while (! done) 347 | avail = IO.select([@read_fh, stdin]) 348 | avail[0].each do |fh| 349 | if (fh == stdin) 350 | if (stdin.eof?) 351 | done = true 352 | else 353 | c = stdin.read_nonblock(1) 354 | $stdout.flush 355 | @write_fh.write(c) 356 | @write_fh.flush 357 | end 358 | elsif (fh == @read_fh) 359 | if (@read_fh.eof?) 360 | done = true 361 | else 362 | $stdout.write(@read_fh.read_nonblock(1024)) 363 | $stdout.flush 364 | end 365 | end 366 | end 367 | end 368 | end 369 | 370 | def read_proc 371 | begin 372 | ready = IO.select([@read_fh], nil, nil, 1) 373 | unless (ready.nil? || ready.size == 0) 374 | if (@read_fh.eof?) 375 | @read_fh.close 376 | return false 377 | else 378 | input = @read_fh.readpartial(4096) 379 | @buffer << input 380 | if @logger.info? 381 | @log_buffer << input 382 | log_buffer 383 | end 384 | end 385 | end 386 | rescue EOFError => e 387 | rescue Errno::EIO => e 388 | @read_fh.close 389 | return false 390 | rescue Exception => e 391 | unless (e.to_s == 'stream closed') 392 | @logger.error("Exception in read_loop:") 393 | @logger.error("#{e}") 394 | @logger.error("\t#{e.backtrace.join("\n\t")}") 395 | end 396 | @read_fh.close 397 | return false 398 | end 399 | return true 400 | end 401 | 402 | ##### 403 | # This method will convert any strings in the argument list to regular 404 | # expressions that search for the literal string 405 | # 406 | # +patterns+:: 407 | # List of patterns to escape 408 | # 409 | def pattern_escape *patterns 410 | escaped_patterns = [] 411 | patterns.each do |pattern| 412 | if (pattern.is_a?(String)) 413 | pattern = Regexp.new(Regexp.escape(pattern)) 414 | elsif (! pattern.is_a?(Regexp)) 415 | raise "Don't know how to match on a #{pattern.class}" 416 | end 417 | escaped_patterns.push(pattern) 418 | end 419 | escaped_patterns 420 | end 421 | end 422 | end 423 | 424 | --------------------------------------------------------------------------------