expectr 1.1.1 → 2.0.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.
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