chump 0.5.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.
Files changed (8) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +21 -0
  5. data/README.md +3 -0
  6. data/chump.gemspec +13 -0
  7. data/lib/chump.rb +407 -0
  8. metadata +48 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e1be23f0e3418c51c4d7f92ef46ac380670656ef17f4e5b0e2f8401654cdfc14
4
+ data.tar.gz: 80117945172f5bbe8a1e7bd5d577ea6a6b941817688e341dfeae43dda5f3e52d
5
+ SHA512:
6
+ metadata.gz: 8b510b9f2d850d4a2a598de5082a4491e6ff44d6df24f97a58baa485d67c9c960728684ac56508f3e9268eb3727ecfcfa8c2c311e5f558a6f8ee413e72864999
7
+ data.tar.gz: 06da95fa869d47cd80cceef849933a875b6def6199c14b20f2a3448ff5d367d3904492c4d0ef9b53922aea8a2882731de8900c77b48b03f4c2f197eeef4bc574
@@ -0,0 +1 @@
1
+ 2.5
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Steve Shreeve
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ ## chump
2
+
3
+ Chump is an interactive session scripting tool
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "chump"
5
+ s.version = "0.5.0"
6
+ s.author = "Steve Shreeve"
7
+ s.email = "steve.shreeve@gmail.com"
8
+ s.summary = "Chump is an interactive session scripting tool"
9
+ s.description = "Chump can be used to easily script terminal interactions."
10
+ s.homepage = "https://github.com/shreeve/chump"
11
+ s.license = "MIT"
12
+ s.files = `git ls-files`.split("\n") - %w[.gitignore]
13
+ end
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # =============================================================================
4
+ # chump.rb: Expect-like utility for automating interactive sessions
5
+ #
6
+ # Steve Shreeve <steve.shreeve@gmail.com>
7
+ #
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation; version 2 of the License.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ # =============================================================================
17
+
18
+ require 'socket'
19
+
20
+ STDIN.sync = STDOUT.sync = STDERR.sync = true
21
+ UNIX = RUBY_PLATFORM =~ /linux|darwin/
22
+
23
+ class IO
24
+ def self.socketpair(sync=true)
25
+ if UNIX
26
+ one, two = UNIXSocket.socketpair
27
+ else
28
+ tcp = TCPServer.new('127.0.0.1', 0)
29
+ one = TCPSocket.new('127.0.0.1', tcp.addr[1])
30
+ two = tcp.accept and tcp.close
31
+ end
32
+ one.sync = two.sync = true if sync
33
+ [one, two]
34
+ end
35
+ end
36
+
37
+ class Chump
38
+ attr_accessor :slow
39
+
40
+ def self.connect(url, opts={})
41
+ scheme, _, target, *info = url.split('/') # scheme://target/info
42
+ target =~ /^(?:(\w+)(?::([^@]+))?@)?(?:([^:]+)?(?::(\d+))?)$/
43
+ user, pass, host, port = $1,$2,$3,$4 # user:pass@host:port
44
+
45
+ opts[:url ] = "#{scheme}//" << [user, host].compact.join('@')
46
+ opts[:url ] << ":#{port}" if host && port
47
+ opts[:url ] << ['', *info].compact.join('/')
48
+ opts[:info] = info unless info.empty?
49
+
50
+ case scheme
51
+ when 'spawn:' then spawn(target, opts)
52
+ when 'ssh:' then ssh(host, port, user, pass, opts)
53
+ when 'tcp:' then tcp(host, port, user, pass, opts)
54
+ else abort "can't parse #{url.inspect}"
55
+ end
56
+ end
57
+
58
+ def self.spawn(cmd=nil, opts={})
59
+ io, child_io = IO.socketpair
60
+
61
+ if UNIX
62
+ require 'pty'
63
+ cmd = ENV['SHELL'].dup if !cmd || cmd.empty?
64
+ cmd << '; cat -' # hack to keep program running, anything better?
65
+ reader, writer, pid = PTY.spawn(cmd); reader.sync = writer.sync = true
66
+ Thread.new { child_io.syswrite(reader.sysread(1 << 16)) while true }
67
+ Thread.new { writer.syswrite(child_io.sysread(1 << 16)) while true }
68
+ at_exit do
69
+ Process.kill(9, pid)
70
+ end
71
+ else
72
+ require 'win32/process'
73
+ child = Process.create(
74
+ 'app_name' => "cmd /k #{cmd}",
75
+ 'process_inherit' => true, #!# is this needed?
76
+ 'thread_inherit' => true, #!# is this needed?
77
+ 'startup_info' => {
78
+ 'stdin' => child_io,
79
+ 'stdout' => child_io,
80
+ 'stderr' => File.open('nul', 'wb') # ignore STDERR (what about sync? close?)
81
+ }
82
+ )
83
+ at_exit do
84
+ Process.TerminateProcess(child.process_handle, child.process_id)
85
+ Process.CloseHandle(child.process_handle)
86
+ end
87
+ child_io.close
88
+ end
89
+
90
+ opts[:io] ? io : new(io, opts)
91
+ end
92
+
93
+ def self.ssh(host, port=nil, user=nil, pass=nil, cmd=nil, opts={})
94
+ io, child_io = IO.socketpair
95
+
96
+ require 'net/ssh'
97
+ ENV['HOME'] ||= ENV['USERPROFILE'] unless UNIX
98
+ options = {}; options[:pass] = pass if pass; options[:port] = port if port
99
+ ssh = Net::SSH.start(host||'localhost', user||ENV['USER']||ENV['USERNAME'], options)
100
+ ssh.open_channel do |channel|
101
+ channel.request_pty do |ch, success|
102
+ raise "can't get pty" unless success
103
+ end
104
+ channel.send_channel_request "shell" do |ch, success|
105
+ raise "can't start shell" unless success
106
+ ch.send_data "#{cmd}\r" if cmd && !cmd.empty?
107
+ Thread.new do
108
+ loop do
109
+ if select([child_io], nil, nil, 0.25)
110
+ data = child_io.sysread(1 << 16) or raise "can't read from child_io"
111
+ ch.send_data(data)
112
+ ssh.process
113
+ end
114
+ end
115
+ end
116
+ end
117
+ channel.on_data do |ch, data|
118
+ child_io.syswrite(data)
119
+ end
120
+ end
121
+ Thread.new { ssh.loop }
122
+
123
+ opts[:io] ? io : new(io, opts)
124
+ end
125
+
126
+ def self.tcp(host, port, user=nil, pass=nil, opts={})
127
+ user,opts = nil,user if user.is_a?(Hash) && pass==nil && opts.empty? # allow short calls
128
+ host ||= "127.0.0.1"
129
+ port ||= 23
130
+ pass ||= "" if user
131
+
132
+ io = TCPSocket.new(host, port.to_i)
133
+
134
+ opts[:auth] ||= {
135
+ # /\xFF\xFD(.)/ => proc { [:pure, "\xFF\xFC#{$1}" ] }, # reject these (do -> wont)
136
+ # /\xFF\xFB(.)/ => proc { [:pure, "\xFF\xFE#{$1}" ] }, # accept these (will -> dont)
137
+ /Log ?in|User ?name/i => [user],
138
+ /Pass ?word/i => pass,
139
+ :else => nil,
140
+ } if user && pass
141
+
142
+ opts[:io] ? io : new(io, opts)
143
+ end
144
+
145
+ def initialize(io, opts={})
146
+ opts.empty? or opts.each {|k,v| opts[k.to_sym] ||= v if k.is_a?(String)}
147
+ @live = opts.has_key?(:live) ? opts[:live] : true # live reads
148
+ @nocr = opts.has_key?(:nocr) ? opts[:nocr] : true # strip "\r"
149
+ @ansi = opts.has_key?(:ansi) ? opts[:ansi] : false # allow ANSI escapes => for GT.M, but checkout "U $P:(NOECHO)"
150
+ @show = opts.has_key?(:show) ? opts[:show] : false # show matches
151
+ @echo = opts.has_key?(:echo) ? opts[:echo] : false # echo sends
152
+ @wait = opts.has_key?(:wait) ? opts[:wait] : nil # sleep times
153
+ @bomb = opts.has_key?(:bomb) ? opts[:bomb] : true # bomb on slow timeout
154
+ @slow = opts.has_key?(:slow) ? opts[:slow] : 10 # slow timeout
155
+ @fast = opts.has_key?(:fast) ? opts[:fast] : 0.25 # fast timeout
156
+ @size = opts.has_key?(:size) ? opts[:size] : 1 << 16 # buffer size
157
+ @line = opts.has_key?(:line) ? opts[:line] : "\r" # line terminator
158
+ @buff = ''
159
+
160
+ @start = Time.now
161
+ @sleep = 0.0
162
+ @final = @start
163
+
164
+ @io = io.is_a?(String) ? self.class.connect(io,opts.update(:io=>true)) : io
165
+ @io.sync = true
166
+
167
+ chat(opts.delete(:auth)) if opts[:auth] # authenticate if requested
168
+ chat(opts.delete(:init)) if opts[:init] # initialize if requested
169
+ end
170
+
171
+ def chat(*list)
172
+ return self if list.empty?
173
+ item = nil
174
+ back = nil
175
+ talk = false
176
+ fast = false
177
+ list.each do |item|
178
+ loop do
179
+ case item
180
+ when false, Symbol # notifier
181
+ back = item
182
+ case back
183
+ when :redo then break
184
+ else return back
185
+ end
186
+ break
187
+ when true, nil # continuer
188
+ back = item
189
+ talk = !talk if item.nil?
190
+ break
191
+ when String, Fixnum, Float # [literal]
192
+ item = item.to_s
193
+ if talk # talker
194
+ send(item)
195
+ back = item
196
+ talk = false
197
+ break
198
+ elsif index = @buff.index(item) # comparer
199
+ @last = item # save for future reference
200
+ back = @buff.slice!(0..(index + item.size - 1))
201
+ print back.tr("\r",'') if @show
202
+ talk = true
203
+ break
204
+ end
205
+ when Regexp # matcher
206
+ if match = @buff.match(item)
207
+ @last = match[1] || match[0] # save for future reference
208
+ @buff = match.post_match
209
+ back = [match.pre_match + match.to_s, *match.to_a[1..-1]]
210
+ print back.first.tr("\r",'') if @show
211
+ talk = true
212
+ break
213
+ else
214
+ talk = false
215
+ end
216
+ when Hash # multiplexer
217
+ item.each do |key, val|
218
+ key, val = '', item[:else] if fast
219
+ case key
220
+ when :else # insurer
221
+ next
222
+ when Symbol # yielder
223
+ case val
224
+ when String, Fixnum, Float # comparer
225
+ val = val.to_s
226
+ if index = @buff.index(val)
227
+ back = @buff.slice!(0..(index + val.size - 1))
228
+ print back.tr("\r",'') if @show
229
+ back = yield(key, back) if block_given?
230
+ break
231
+ end
232
+ when Regexp # matcher
233
+ if match = @buff.match(val)
234
+ @buff = match.post_match
235
+ back = [match.pre_match + match.to_s, *match.to_a[1..-1]]
236
+ print back.first.tr("\r",'') if @show
237
+ back = yield(key, back) if block_given?
238
+ break
239
+ end
240
+ when Array, Proc, Hash # indexer
241
+ # processed elsewhere
242
+ else
243
+ raise "Hash symbols don't support #{val.class} matchers"
244
+ end
245
+ when String, Fixnum, Float, Regexp # comparer/matcher (ugly, but shares actions)
246
+ key = key.to_s unless regx = key.is_a?(Regexp)
247
+ if fast
248
+ back = :else
249
+ elsif !regx && index = @buff.index(key)
250
+ back = @buff.slice!(0..(index + key.size - 1))
251
+ print back.tr("\r",'') if @show
252
+ elsif regx && match = @buff.match(key)
253
+ @buff = match.post_match
254
+ back = [match.pre_match + match.to_s, *match.to_a[1..-1]]
255
+ print back.first.tr("\r",'') if @show
256
+ else
257
+ regx = nil
258
+ end
259
+ unless regx.nil?
260
+ case val
261
+ when String, Fixnum, Float
262
+ send(val.to_s)
263
+ when Array
264
+ back = chat(nil, *val) unless val.empty?
265
+ back = :redo if val.size <= 1
266
+ when Proc
267
+ eval("proc {|m| $~ = m}", val.binding).call($~) if $~ # infuse proc with our match variables
268
+ back = back.is_a?(String) ? val.call(back) : val.call(*back) # don't convert embedded newlines to array
269
+ case val = back
270
+ when Array
271
+ if pure = (val.first == :pure)
272
+ line, @line = @line, ""
273
+ back = chat(nil, *val[1..-1]) unless val.size == 1
274
+ @line = line
275
+ back = :redo if val.size <= 2
276
+ else
277
+ back = chat(nil, *val) unless val.empty?
278
+ back = :redo if val.size <= 1
279
+ end
280
+ end
281
+ when false, Symbol
282
+ if val == :this
283
+ back = back.first if back.is_a?(Array) # regexps store leading + matched text in back.first
284
+ else
285
+ back = val
286
+ end
287
+ when true, nil
288
+ back = val
289
+ when Hash
290
+ back = chat(val)
291
+ else
292
+ raise "Hash literals can't multiplex to #{val.class} types"
293
+ end
294
+ break
295
+ end
296
+ else
297
+ raise "Hash items can't process #{key}.class keys"
298
+ end
299
+ end and begin # read when nothing matches
300
+ fast = read(item.has_key?(:else)) == :fast
301
+ next
302
+ end
303
+ fast &&= false
304
+ talk = false
305
+ case back
306
+ when :else then break
307
+ when :redo then redo
308
+ when :skip then return :skip
309
+ when false, Symbol then return back
310
+ end
311
+ break
312
+ when Array # walker
313
+ if item.first == :pure
314
+ ansi, @ansi = @ansi, :false
315
+ line, @line = @line, ""
316
+ back = talk ? chat(nil, *item[1..-1]) : chat(*item[1..-1])
317
+ @ansi = ansi
318
+ @line = line
319
+ else
320
+ back = talk ? chat(nil, *item) : chat(*item)
321
+ end
322
+ talk = false
323
+ break
324
+ when Proc, Method # macro
325
+ item = item.to_proc if item.class == Method
326
+ eval("proc {|m| $~ = m}", item.binding).call($~) if $~ # infuse proc with our match variables
327
+ back = back.is_a?(String) ? item.call(back) : item.call(*back) # don't convert embedded newlines to array
328
+ item = back unless back == :redo
329
+ redo
330
+ else # aborter
331
+ raise "Chump doesn't handle #{item.class} objects like: #{item.inspect}"
332
+ end
333
+ read unless talk
334
+ end
335
+ case back
336
+ when :redo then break
337
+ when :skip then break
338
+ when :false then return false # same as false in parent
339
+ when :true then return true # same as true in parent
340
+ when :nil then return nil # same as nil in parent
341
+ end
342
+ end
343
+ back
344
+ rescue Object => e
345
+ exit if defined?(PTY::ChildExited) and e.class == PTY::ChildExited
346
+ warn ['', '', "==[ #{e} ]==" ] * "\n"
347
+ warn ['', e.backtrace, ''].flatten * "\n"
348
+ warn ['', "Buffer: ", @buff.inspect] * "\n"
349
+ warn ['', "Failed: ", item.inspect ] * "\n" if item
350
+ disconnect
351
+ exit
352
+ end
353
+
354
+ alias :wait :chat
355
+ alias :[] :chat
356
+
357
+ def read(fast=false)
358
+ unless select([@io], nil, nil, fast ? @fast : @slow)
359
+ return :fast if fast
360
+ raise "Timeout" if @bomb
361
+ return :slow
362
+ end
363
+ buff = @io.sysread(@size)
364
+ buff.tr!("\r",'') if @nocr
365
+ unless @ansi
366
+ # http://www.esrl.noaa.gov/gmd/dv/hats/cats/stations/qnxman/Devansi.html
367
+ # http://support.dell.com/support/edocs/systems/SC1425/en/ug/f3593ab0.htm
368
+ buff.gsub!(/\x08/,'')
369
+ buff.gsub!(/\e[=>]/,'')
370
+ buff.gsub!(/\e\[(?>[^a-z]*)[a-z]/i,'')
371
+ end
372
+ print @nocr ? buff : buff.tr("\r",'') if @live
373
+ @buff << buff
374
+ end
375
+
376
+ def unshift(str)
377
+ Thread.exclusive { @buff = str + @buff }
378
+ end
379
+
380
+ def send(item='', *list)
381
+ if back = item
382
+ select(nil, [@io], nil, @slow) or return :slow
383
+ if @wait
384
+ prior = Time.now.to_f
385
+ sleep(@wait[0] + rand * (@wait[1] - @wait[0]))
386
+ @sleep += Time.now.to_f - prior
387
+ end
388
+ back = back.to_s
389
+ @io.syswrite(back + @line) # line ending, usually "\r"
390
+ print back.tr("\r",'') if @echo
391
+ end
392
+ back = chat(*list) unless list.empty?
393
+ back
394
+ end
395
+
396
+ def peek(*list)
397
+ list.compact.inject(:else=>false) {|h,v| h[v]=:this; h}
398
+ end
399
+
400
+ def disconnect
401
+ @stop = Time.now
402
+ print @buff.tr("\r",'') if @show
403
+ @io.close
404
+ puts
405
+ end
406
+
407
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chump
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Shreeve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Chump can be used to easily script terminal interactions.
14
+ email: steve.shreeve@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".ruby-version"
20
+ - Gemfile
21
+ - LICENSE
22
+ - README.md
23
+ - chump.gemspec
24
+ - lib/chump.rb
25
+ homepage: https://github.com/shreeve/chump
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.0.6
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Chump is an interactive session scripting tool
48
+ test_files: []