chump 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +3 -0
- data/chump.gemspec +13 -0
- data/lib/chump.rb +407 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -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
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5
|
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
data/chump.gemspec
ADDED
@@ -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
|
data/lib/chump.rb
ADDED
@@ -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: []
|