expectr 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eb60ae1f64e5710fe3e5cfdff6e49bf938a9aaa3
4
+ data.tar.gz: 98fd24b1cadf73f5c88a313dd48cd1edb0e6f64d
5
+ SHA512:
6
+ metadata.gz: 1748339aa16a902c05c7b19a17e6469a94eaf3a2e21d3143ac92a2291d932cd52a3ecad280607efaa409ec7fb1cd017e5945c09d5a9bafda4a2906800a86a64c
7
+ data.tar.gz: 7d21a1bc05f4067f823d0f0292f1242dcee4bb4952d7df7ca520f8287e5aa4280de8e5b281f4f0b60c9321ae53bf45a76171abc35c7b48ea139067a2e00bc0b2
data/bin/expectr ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'expectr'
4
+ require 'expectr/interpreter'
5
+
6
+ class Expectr
7
+ module Main extend self
8
+ # Public: Read and run source passed in to the interpreter.
9
+ #
10
+ # Returns nothing.
11
+ def run
12
+ src = ARGF.read
13
+ exit 2 unless src
14
+
15
+ interpreter = Expectr::Interpreter.new(src.untaint)
16
+ interpreter.filename = $FILENAME
17
+ interpreter.run
18
+ end
19
+ end
20
+ end
21
+
22
+ Expectr::Main.run
@@ -0,0 +1,50 @@
1
+ require 'expectr'
2
+ require 'expectr/interface'
3
+ require 'expectr/child'
4
+
5
+ class Expectr
6
+ # Internal: The Expectr::Adopt Class contains the interface to interacting
7
+ # with child processes not spawned by Expectr.
8
+ #
9
+ # All methods with the prefix 'interface_' in their name will return a Proc
10
+ # designed to be defined as an instance method in the primary Expectr object.
11
+ # These methods will all be documented as if they are the Proc in question.
12
+ class Adopt < Expectr::Child
13
+ # Public: Initialize a new Expectr::Adopt object.
14
+ # IO Objects are named in such a way as to maintain interoperability with
15
+ # the methods from the Expectr::Child class.
16
+ #
17
+ # stdin - IO object open for writing.
18
+ # stdout - IO object open for reading.
19
+ # pid - FixNum corresponding to the PID of the process being adopted
20
+ # (default: 1)
21
+ #
22
+ # Raises TypeError if arguments are of type other than IO.
23
+ def initialize(stdin, stdout)
24
+ unless stdin.kind_of?(IO) && stdout.kind_of?(IO)
25
+ raise(TypeError, Errstr::IO_EXPECTED)
26
+ end
27
+ @stdin = stdin
28
+ @stdout = stdout
29
+ @stdout.winsize = $stdout.winsize if $stdout.tty?
30
+ end
31
+
32
+ # Public: Present a streamlined interface to create a new Expectr instance.
33
+ #
34
+ # stdout - IO object open for reading.
35
+ # stdin - IO object open for writing.
36
+ # pid - FixNum corresponding to the PID of the process being adopted
37
+ # (default: 1)
38
+ # args - A Hash used to specify options for the new object, per
39
+ # Expectr#initialize.
40
+ #
41
+ # Returns a new Expectr object
42
+ def self.spawn(stdout, stdin, pid = 1, args = {})
43
+ args[:interface] = :adopt
44
+ args[:stdin] = stdin
45
+ args[:stdout] = stdout
46
+ args[:pid] = pid
47
+ Expectr.new('', args)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,165 @@
1
+ require 'pty'
2
+ require 'expectr/interface'
3
+
4
+ class Expectr
5
+ # Internal: The Expectr::Child class contains the interface to interacting
6
+ # with child processes.
7
+ #
8
+ # All methods with the prefix 'interface_' in their name will return a Proc
9
+ # designed to be defined as an instance method in the primary Expectr object.
10
+ # These methods will all be documented as if they are the Proc in question.
11
+ class Child
12
+ include Expectr::Interface
13
+ attr_reader :stdin
14
+ attr_reader :stdout
15
+ attr_reader :pid
16
+
17
+ # Public: Initialize a new Expectr::Child object.
18
+ # Spawns a sub-process and attaches to STDIN and STDOUT for the new
19
+ # process.
20
+ #
21
+ # cmd - A String or File referencing the application to launch.
22
+ #
23
+ # Raises TypeError if argument is anything other than String or File.
24
+ def initialize(cmd)
25
+ cmd = cmd.path if cmd.kind_of?(File)
26
+ unless cmd.kind_of?(String)
27
+ raise(TypeError, Errstr::STRING_FILE_EXPECTED)
28
+ end
29
+
30
+ @stdout,@stdin,@pid = PTY.spawn(cmd)
31
+ @stdout.winsize = $stdout.winsize if $stdout.tty?
32
+ end
33
+
34
+ # Public: Send a signal to the running child process.
35
+ #
36
+ # signal - Symbol, String or FixNum corresponding to the symbol to be sent
37
+ # to the running process. (default: :TERM)
38
+ #
39
+ # Returns a boolean indicating whether the process was successfully sent
40
+ # the signal.
41
+ # Raises ProcessError if the process is not running (@pid = 0).
42
+ def interface_kill!
43
+ ->(signal = :TERM) {
44
+ unless @pid > 0
45
+ raise(ProcessError, Errstr::PROCESS_NOT_RUNNING)
46
+ end
47
+ Process::kill(signal.to_sym, @pid) == 1
48
+ }
49
+ end
50
+
51
+ # Public: Send input to the active child process.
52
+ #
53
+ # str - String to be sent.
54
+ #
55
+ # Returns nothing.
56
+ # Raises Expectr::ProcessError if the process is not running (@pid = 0)
57
+ def interface_send
58
+ ->(str) {
59
+ begin
60
+ @stdin.syswrite str
61
+ rescue Errno::EIO #Application is not running
62
+ @pid = 0
63
+ end
64
+ unless @pid > 0
65
+ raise(Expectr::ProcessError, Errstr::PROCESS_GONE)
66
+ end
67
+ }
68
+ end
69
+
70
+ # Public: Read the child process's output, force UTF-8 encoding, then
71
+ # append to the internal buffer and print to $stdout if appropriate.
72
+ #
73
+ # Returns nothing.
74
+ def interface_output_loop
75
+ -> {
76
+ while @pid > 0
77
+ unless select([@stdout], nil, nil, @timeout).nil?
78
+ buf = ''
79
+
80
+ begin
81
+ @stdout.sysread(@buffer_size, buf)
82
+ rescue Errno::EIO #Application is not running
83
+ @pid = 0
84
+ return
85
+ end
86
+ process_output(buf)
87
+ end
88
+ end
89
+ }
90
+ end
91
+
92
+ def interface_prepare_interact_environment
93
+ -> {
94
+ env = {sig: {}}
95
+
96
+ # Save old tty settings and set up the new environment
97
+ env[:tty] = `stty -g`
98
+ `stty -icanon min 1 time 0 -echo`
99
+
100
+ # SIGINT should be sent to the child as \C-c
101
+ env[:sig]['INT'] = trap 'INT' do
102
+ send "\C-c"
103
+ end
104
+
105
+ # SIGTSTP should be sent to the process as \C-z
106
+ env[:sig]['TSTP'] = trap 'TSTP' do
107
+ send "\C-z"
108
+ end
109
+
110
+ # SIGWINCH should trigger an update to the child processes window size
111
+ env[:sig]['WINCH'] = trap 'WINCH' do
112
+ @stdout.winsize = $stdout.winsize
113
+ end
114
+
115
+ env
116
+ }
117
+ end
118
+
119
+ # Public: Create a Thread containing the loop which is responsible for
120
+ # handling input from the user in interact mode.
121
+ #
122
+ # Returns a Thread containing the running loop.
123
+ def interface_interact_thread
124
+ -> {
125
+ @interact = true
126
+ env = prepare_interact_environment
127
+ Thread.new do
128
+ begin
129
+ input = ''
130
+
131
+ while @pid > 0 && @interact
132
+ if select([$stdin], nil, nil, 1)
133
+ c = $stdin.getc.chr
134
+ send c unless c.nil?
135
+ end
136
+ end
137
+ ensure
138
+ restore_environment(env)
139
+ end
140
+ end
141
+ }
142
+ end
143
+
144
+ # Public: Return the PTY's window size.
145
+ #
146
+ # Returns a two-element Array (same as IO#winsize)
147
+ def interface_winsize
148
+ -> {
149
+ @stdout.winsize
150
+ }
151
+ end
152
+
153
+ # Public: Present a streamlined interface to create a new Expectr instance.
154
+ #
155
+ # cmd - A String or File referencing the application to launch.
156
+ # args - A Hash used to specify options for the new object, per
157
+ # Expectr#initialize.
158
+ #
159
+ # Returns a new Expectr object
160
+ def self.spawn(cmd, args = {})
161
+ args[:interface] = :child
162
+ Expectr.new(cmd, args)
163
+ end
164
+ end
165
+ end
data/lib/expectr/error.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  class Expectr
2
- # Public: Error to denote a problem with the running Process associated with
3
- # the current Expectr object
2
+ # Public: Denotes a problem with the running Process associated with a
3
+ # Child Interface
4
4
  class ProcessError < StandardError; end
5
+
6
+ module Interface
7
+ # Public: Denotes an interface which cannot be killed (e.g. Lambda Interface)
8
+ class NotKillableError < StandardError; end
9
+ end
10
+
11
+ class Interpreter
12
+ # Public: Denotes that input was attempted to be sent to a non-existant
13
+ # process.
14
+ class NotRunningError < StandardError; end
15
+ end
5
16
  end
@@ -0,0 +1,33 @@
1
+ class Expectr
2
+ module Errstr
3
+ EXPECT_WRONG_TYPE = "Pattern should be of class String, Regexp, or Hash"
4
+ ALREADY_INTERACT = "Already in interact mode"
5
+ end
6
+
7
+ class Child
8
+ module Errstr
9
+ STRING_FILE_EXPECTED = "Command should be of type String or File"
10
+ PROCESS_NOT_RUNNING = "No process is running"
11
+ PROCESS_GONE = "Child process no longer exists"
12
+ end
13
+ end
14
+
15
+ class Adopt < Child
16
+ module Errstr
17
+ IO_EXPECTED = "Arguments of type IO expected"
18
+ end
19
+ end
20
+
21
+ class Interpreter
22
+ module Errstr
23
+ BOOLEAN_OR_FIXNUM = "Boolean or Fixnum expected, received a %s"
24
+ PROCESS_NOT_RUNNING = "No process is running"
25
+ end
26
+ end
27
+
28
+ class Lambda
29
+ module Errstr
30
+ PROC_EXPECTED = "Proc Objects expected for reader and writer"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ class Expectr
2
+ module Interface
3
+ # Public: Enumerate and expose all interface functions for a given
4
+ # Interface.
5
+ #
6
+ # Returns a two-dimensional Array containing a name for the new method and
7
+ # a reference to the extant method.
8
+ def init_instance
9
+ methods = []
10
+ public_methods.select { |m| m =~ /^interface_/ }.each do |name|
11
+ method = public_method(name)
12
+ name = name.to_s.gsub(/^interface_/, '').to_sym
13
+ methods << [name, method]
14
+ end
15
+ methods
16
+ end
17
+
18
+ # Public: Return a Thread which does nothing, representing an interface
19
+ # with no functional interact environment available.
20
+ #
21
+ # Returns a Thread.
22
+ def interface_interact_thread
23
+ -> {
24
+ Thread.new { }
25
+ }
26
+ end
27
+
28
+ # Public: Return an empty Hash representing a case where no action needed
29
+ # to be taken in order to prepare the environment for interact mode.
30
+ #
31
+ # Returns an empty Hash.
32
+ def interface_prepare_interact_interface
33
+ -> {
34
+ {}
35
+ }
36
+ end
37
+
38
+ # Internal: Restore environment after interact mode has been left.
39
+ #
40
+ # Returns nothing.
41
+ def interface_restore_environment
42
+ ->(env) {
43
+ env[:sig].each do |signal, handler|
44
+ trap signal, handler
45
+ end
46
+ unless env[:tty].nil?
47
+ `stty #{env[:tty]}`
48
+ end
49
+ @interact = false
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,119 @@
1
+ require 'expectr'
2
+
3
+ class Expectr
4
+ # Internal: Provide an interface to Expectr in a manner designed to mimic
5
+ # Expect's original functionality.
6
+ class Interpreter
7
+ DEFAULT_TIMEOUT = 10
8
+
9
+ # Public: Filename of currently executing script.
10
+ attr_accessor :filename
11
+
12
+ # Public: Initialize a new Expectr interface.
13
+ #
14
+ # source - String containing the source to be executed.
15
+ def initialize(source)
16
+ @source = source
17
+ @variables = { timeout: DEFAULT_TIMEOUT }
18
+ @expect = nil
19
+ end
20
+
21
+ # Public: Run the source associated with the Interface object, bound to the
22
+ # Object.
23
+ #
24
+ # Returns nothing.
25
+ def run
26
+ eval(@source, binding, (@filename || "(expectr)"), 0)
27
+ end
28
+
29
+ # Public: Print one or more messages to $stdout.
30
+ #
31
+ # str - String or Array of Strings to print to $stdout.
32
+ #
33
+ # Returns nothing.
34
+ def send_user(*str)
35
+ str.each { |line| $stdout.print line }
36
+ end
37
+
38
+ # Public: Set whether output from the active Expectr object should be
39
+ # echoed to the user.
40
+ #
41
+ # enable - Boolean value denoting whether output to the screen should be
42
+ # enabled.
43
+ # In order to keep compatibility with Expect, a Fixnum can be
44
+ # passed, such that any number greater than 0 is evaluated as
45
+ # true, and 0 or less is false.
46
+ #
47
+ # Returns nothing.
48
+ # Raises Argument
49
+ def log_user(enable)
50
+ if enable.is_a?(Numeric)
51
+ enable = (enable > 0)
52
+ end
53
+ unless enable.is_a?(TrueClass) || enable.is_a?(FalseClass)
54
+ raise(TypeError, Errstr::BOOLEAN_OR_FIXNUM % enable.class.name)
55
+ end
56
+
57
+ set(:flush_buffer, enable)
58
+ end
59
+
60
+ # Public: Spawn an instance of a command via Expectr.
61
+ #
62
+ # cmd - String referencing the application to spawn.
63
+ #
64
+ # Returns nothing.
65
+ def spawn(cmd)
66
+ @expect = Expectr.new(cmd, @variables)
67
+ end
68
+
69
+ # Public: Provide an interface to Expectr#expect.
70
+ #
71
+ # args - Arguments to be passed through to the Expectr object.
72
+ #
73
+ # Returns per Expectr#expect.
74
+ def expect(*args)
75
+ @expect.expect(*args)
76
+ end
77
+
78
+ # Public: Send a String to the process referenced by the active Expectr
79
+ # object.
80
+ #
81
+ # str - String to send to the process.
82
+ #
83
+ # Returns nothing.
84
+ # Raises NotRunningError if no Expectr object is active.
85
+ def send(str)
86
+ if @expect.nil?
87
+ raise(NotRunningError, Errstr::PROCESS_NOT_RUNNING)
88
+ end
89
+ @expect.send(str)
90
+ end
91
+
92
+ # Public: Terminate any process associated with the Interpreter.
93
+ #
94
+ # Returns nothing.
95
+ def close
96
+ @expect.kill!(:KILL) if @expect.respond_to?(:kill!)
97
+ rescue Expectr::ProcessError
98
+ ensure
99
+ @expect = nil
100
+ end
101
+
102
+ private
103
+
104
+ # Internal: Set value in internal Hash and update attr_accessor value in
105
+ # the active Expectr object if applicable.
106
+ #
107
+ # variable_name - Name of key to set in the internal Hash.
108
+ # value - Value to associate with the key.
109
+ #
110
+ # Returns nothing.
111
+ def set(variable_name, value)
112
+ @variables[variable_name] = value
113
+ method_name = (variable_name.to_s + "=").to_sym
114
+ if @expect.methods.include?(method_name)
115
+ @expect.method(method_name).call(value)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,131 @@
1
+ require 'expectr'
2
+ require 'expectr/interface'
3
+
4
+ class Expectr
5
+ # Internal: The Expectr::Lambda Class contains the interface to interacting
6
+ # with ruby objects transparently via lambdas in a manner consistent with
7
+ # other Expectr interfaces.
8
+ #
9
+ # All methods with the prefix 'interface_' in their name will return a Proc
10
+ # designed to be defined as an instance method in the primary Expectr object.
11
+ # These methods will all be documented as if they are the Proc in question.
12
+ class Lambda
13
+ include Expectr::Interface
14
+ attr_reader :reader
15
+ attr_reader :writer
16
+
17
+ # Public: Initialize a new Expectr::Lambda object.
18
+ #
19
+ # reader - Lambda which is meant to be interacted with as if it were
20
+ # analogous to STDIN for a child process.
21
+ # writer - Lambda which is meant to be interacted with as if it were
22
+ # analogous to STDOUT for a child process.
23
+ #
24
+ # Raises TypeError if arguments aren't of type Proc.
25
+ def initialize(reader, writer)
26
+ unless reader.kind_of?(Proc) && writer.kind_of?(Proc)
27
+ raise(TypeError, Errstr::PROC_EXPECTED)
28
+ end
29
+
30
+ @reader = reader
31
+ @writer = writer
32
+ end
33
+
34
+ # Public: Present a streamlined interface to create a new Expectr instance.
35
+ #
36
+ # reader - Lambda which is meant to be interacted with as if it were
37
+ # analogous to STDIN for a child process.
38
+ # writer - Lambda which is meant to be interacted with as if it were
39
+ # analogous to STDOUT for a child process.
40
+ # args - A Hash used to specify options for the new object, per
41
+ # Expectr#initialize.
42
+ #
43
+ # Returns a new Expectr object
44
+ def self.spawn(reader, writer, args = {})
45
+ args[:interface] = :lambda
46
+ args[:reader] = reader
47
+ args[:writer] = writer
48
+ Expectr.new('', args)
49
+ end
50
+
51
+ # Public: Send input to the active child process.
52
+ #
53
+ # args - Arguments to pass to the reader interface.
54
+ #
55
+ # Returns nothing.
56
+ def interface_send
57
+ ->(args) {
58
+ @reader.call(*args)
59
+ }
60
+ end
61
+
62
+ # Public: Prepare the operating environment for interact mode, set the
63
+ # interact flag to true.
64
+ #
65
+ # Returns a Hash containing old signal handlers and tty parameters.
66
+ def interface_prepare_interact_environment
67
+ -> {
68
+ env = {sig: {}}
69
+
70
+ # Save old tty settings and set up the new environment
71
+ env[:tty] = `stty -g`
72
+ `stty -icanon min 1 time 0 -echo`
73
+
74
+ # SIGINT should be sent to the child as \C-c
75
+ env[:sig]['INT'] = trap 'INT' do
76
+ send "\C-c"
77
+ end
78
+
79
+ # SIGTSTP should be sent to the process as \C-z
80
+ env[:sig]['TSTP'] = trap 'TSTP' do
81
+ send "\C-z"
82
+ end
83
+
84
+ @interact = true
85
+ env
86
+ }
87
+ end
88
+
89
+ # Public: Create a Thread containing the loop which is responsible for
90
+ # handling input from the user in interact mode.
91
+ #
92
+ # Returns a Thread containing the running loop.
93
+ def interface_interact_thread
94
+ -> {
95
+ Thread.new do
96
+ env = prepare_interact_environment
97
+ input = ''
98
+
99
+ while @interact
100
+ if select([$stdin], nil, nil, 1)
101
+ c = $stdin.getc.chr
102
+ send c unless c.nil?
103
+ end
104
+ end
105
+
106
+ restore_environment(env)
107
+ end
108
+ }
109
+ end
110
+
111
+ # Internal: Call the writer lambda, reading the output, forcing UTF-8 and
112
+ # appending to the internal buffer and printing to $stdout if appropriate.
113
+ #
114
+ # Returns nothing.
115
+ def interface_output_loop
116
+ -> {
117
+ buf = ''
118
+ loop do
119
+ buf.clear
120
+
121
+ begin
122
+ buf << @writer.call.to_s
123
+ rescue Errno::EIO # Lambda is signaling that execution should end.
124
+ return
125
+ end
126
+ process_output(buf)
127
+ end
128
+ }
129
+ end
130
+ end
131
+ end
@@ -1,3 +1,3 @@
1
1
  class Expectr
2
- VERSION = '1.1.1'
2
+ VERSION = '2.0.0'
3
3
  end