chump 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []