senv 0.4.2

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.
@@ -0,0 +1,527 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'base64'
6
+ require 'securerandom'
7
+ require 'fileutils'
8
+ require 'pathname'
9
+ require 'set'
10
+ require 'openssl'
11
+ require 'uri'
12
+ require 'cgi'
13
+ require 'shellwords'
14
+ require 'tmpdir'
15
+ require 'tempfile'
16
+ require 'pp'
17
+ require 'open3'
18
+
19
+ #
20
+ module Senv
21
+ class Script
22
+ attr_accessor :source
23
+ attr_accessor :root
24
+ attr_accessor :env
25
+ attr_accessor :argv
26
+ attr_accessor :stdout
27
+ attr_accessor :stdin
28
+ attr_accessor :stderr
29
+ attr_accessor :help
30
+
31
+ def run!(env = ENV, argv = ARGV)
32
+ before!
33
+ init!(env, argv)
34
+ parse_command_line!
35
+ set_mode!
36
+ run_mode!
37
+ after!
38
+ end
39
+
40
+ def init!(env, argv)
41
+ @klass = self.class
42
+ @env = env.to_hash.dup
43
+ @argv = argv.map{|arg| arg.dup}
44
+ @stdout = $stdout.dup
45
+ @stdin = $stdin.dup
46
+ @stderr = $stderr.dup
47
+ @help = @klass.help || Util.unindent(DEFAULT_HELP)
48
+ end
49
+
50
+ def self.before(&block)
51
+ @before ||= []
52
+ @before << block if block
53
+ @before
54
+ end
55
+
56
+ def before!
57
+ self.class.before.each{|block| block.call}
58
+ end
59
+
60
+ def self.after(&block)
61
+ @after ||= []
62
+ @after << block if block
63
+ @after
64
+ end
65
+
66
+ def after!
67
+ self.class.after.each{|block| block.call}
68
+ end
69
+
70
+ DEFAULT_HELP = <<-__
71
+ NAME
72
+ #TODO
73
+
74
+ SYNOPSIS
75
+ #TODO
76
+
77
+ DESCRIPTION
78
+ #TODO
79
+
80
+ EXAMPLES
81
+ #TODO
82
+ __
83
+
84
+ def parse_command_line!
85
+ @options = Hash.new
86
+ @opts = Hash.new
87
+
88
+ argv = []
89
+ head = []
90
+ tail = []
91
+
92
+ %w[ :: -- ].each do |stop|
93
+ if((i = @argv.index(stop)))
94
+ head = @argv.slice(0 ... i)
95
+ tail = @argv.slice((i + 1) ... @argv.size)
96
+ @argv = head
97
+ break
98
+ end
99
+ end
100
+
101
+ @argv.each do |arg|
102
+ case
103
+ when arg =~ %r`^\s*:([^:\s]+)[=](.+)`
104
+ key = $1
105
+ val = $2
106
+ @options[key] = val
107
+ when arg =~ %r`^\s*(:+)(.+)`
108
+ switch = $1
109
+ key = $2
110
+ val = switch.size.odd?
111
+ @options[key] = val
112
+ else
113
+ argv.push(arg)
114
+ end
115
+ end
116
+
117
+ argv += tail
118
+
119
+ @argv.replace(argv)
120
+
121
+ @options.each do |key, val|
122
+ @opts[key.to_s.to_sym] = val
123
+ end
124
+
125
+ [@options, @opts]
126
+ end
127
+
128
+ def set_mode!
129
+ case
130
+ when respond_to?("run_#{ @argv[0] }")
131
+ @mode = @argv.shift
132
+ else
133
+ @mode = nil
134
+ end
135
+ end
136
+
137
+ def run_mode!
138
+ if @mode
139
+ return send("run_#{ @mode }")
140
+ else
141
+ if respond_to?(:run)
142
+ return send(:run)
143
+ end
144
+
145
+ if @argv.empty?
146
+ run_help
147
+ else
148
+ abort("#{ $0 } help")
149
+ end
150
+ end
151
+ end
152
+
153
+ def run_help
154
+ STDOUT.puts(@help)
155
+ end
156
+
157
+ def help!
158
+ run_help
159
+ abort
160
+ end
161
+
162
+ #
163
+ module Util
164
+ def unindent(arg)
165
+ string = arg.to_s.dup
166
+ margin = nil
167
+ string.each_line do |line|
168
+ next if line =~ %r/^\s*$/
169
+ margin = line[%r/^\s*/] and break
170
+ end
171
+ string.gsub!(%r/^#{ margin }/, "") if margin
172
+ margin ? string : nil
173
+ end
174
+
175
+ def esc(*args)
176
+ args.flatten.compact.map{|arg| Shellwords.escape(arg)}.join(' ')
177
+ end
178
+
179
+ def uuid
180
+ SecureRandom.uuid
181
+ end
182
+
183
+ def tmpname(*args)
184
+ opts = extract_options!(*args)
185
+
186
+ base = opts.fetch(:base){ uuid }.to_s.strip
187
+ ext = opts.fetch(:ext){ 'tmp' }.to_s.strip.sub(/^[.]+/, '')
188
+ basename = opts.fetch(:basename){ "#{ base }.#{ ext }" }
189
+
190
+ File.join(Dir.tmpdir, basename)
191
+ end
192
+
193
+ def tmpfile(*args, &block)
194
+ opts = extract_options!(args)
195
+
196
+ path = tmpname(opts)
197
+
198
+
199
+ tmp = open(path, 'w+')
200
+ tmp.binmode
201
+ tmp.sync = true
202
+
203
+ unless args.empty?
204
+ src = args.join
205
+ tmp.write(src)
206
+ tmp.flush
207
+ tmp.rewind
208
+ end
209
+
210
+ if block
211
+ begin
212
+ block.call(tmp)
213
+ ensure
214
+ FileUtils.rm_rf(path)
215
+ end
216
+ else
217
+ at_exit{ Kernel.system("rm -rf #{ esc(path) }") }
218
+ return tmp
219
+ end
220
+ end
221
+
222
+ def extract_options!(args)
223
+ unless args.is_a?(Array)
224
+ args = [args]
225
+ end
226
+
227
+ opts = args.last.is_a?(Hash) ? args.pop : {}
228
+
229
+ symbolize_keys!(opts)
230
+
231
+ return opts
232
+ end
233
+
234
+ def extract_options(args)
235
+ opts = extract_options!(args)
236
+
237
+ args.push(opts)
238
+
239
+ opts
240
+ end
241
+
242
+ def symbolize_keys!(hash)
243
+ hash.keys.each do |key|
244
+ if key.is_a?(String)
245
+ val = hash.delete(key)
246
+
247
+ if val.is_a?(Hash)
248
+ symbolize_keys!(val)
249
+ end
250
+
251
+ hash[key.to_s.gsub('-', '_').to_sym] = val
252
+ end
253
+ end
254
+
255
+ return hash
256
+ end
257
+
258
+ def symbolize_keys(hash)
259
+ symbolize_keys!(copy(hash))
260
+ end
261
+
262
+ def copy(object)
263
+ Marshal.load(Marshal.dump(object))
264
+ end
265
+
266
+ def debug!(arg)
267
+ if arg.is_a?(String)
268
+ warn "[DEBUG] #{ arg }"
269
+ else
270
+ warn "[DEBUG] >\n#{ arg.to_yaml rescue arg.pretty_inspect }"
271
+ end
272
+ end
273
+
274
+ def debug(arg)
275
+ debug!(arg) if debug?
276
+ end
277
+
278
+ def debug?
279
+ ENV['SCRIPT_DEBUG'] || ENV['DEBUG']
280
+ end
281
+
282
+ def sys!(*args, &block)
283
+ opts = extract_options!(args)
284
+
285
+ cmd = args
286
+
287
+ debug(:cmd => cmd)
288
+
289
+ open3 = (
290
+ block ||
291
+ opts[:stdin] ||
292
+ opts[:quiet] ||
293
+ opts[:capture]
294
+ )
295
+
296
+ if(open3)
297
+ stdin = opts[:stdin]
298
+ stdout = ''
299
+ stderr = ''
300
+ status = nil
301
+
302
+ Open3.popen3(*cmd) do |i, o, e, t|
303
+ ot = async_reader_thread_for(o, stdout)
304
+ et = async_reader_thread_for(e, stderr)
305
+
306
+ i.write(stdin) if stdin
307
+ i.close
308
+
309
+ ot.join
310
+ et.join
311
+
312
+ status = t.value
313
+ end
314
+
315
+ if status.exitstatus == 0
316
+ result = nil
317
+
318
+ if opts[:capture]
319
+ result = stdout.to_s.strip
320
+ else
321
+ if block
322
+ result = block.call(status, stdout, stderr)
323
+ else
324
+ result = [status, stdout, stderr]
325
+ end
326
+ end
327
+
328
+ return(result)
329
+ else
330
+ if opts[:capture]
331
+ abort("#{ [cmd].join(' ') } #=> #{ status.exitstatus }")
332
+ else
333
+ false
334
+ end
335
+ end
336
+ else
337
+ env = opts[:env] || {}
338
+ argv = [env, *cmd]
339
+ system(*argv) || abort("#{ [cmd].join(' ') } #=> #{ $?.exitstatus }")
340
+ return true
341
+ end
342
+ end
343
+
344
+ def sys(*args, &block)
345
+ begin
346
+ sys!(*args, &block)
347
+ rescue Object
348
+ false
349
+ end
350
+ end
351
+
352
+ def async_reader_thread_for(io, accum)
353
+ Thread.new(io, accum) do |i, a|
354
+ Thread.current.abort_on_exception = true
355
+
356
+ while true
357
+ buf = i.read(8192)
358
+
359
+ if buf
360
+ a << buf
361
+ else
362
+ break
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ def realpath(path)
369
+ Pathname.new(path.to_s).expand_path.realpath.to_s
370
+ end
371
+
372
+ def filelist(*args, &block)
373
+ accum = (block || proc{ Set.new }).call
374
+ raise ArgumentError.new('accum.class != Set') unless accum.is_a?(Set)
375
+
376
+ _ = args.last.is_a?(Hash) ? args.pop : {}
377
+
378
+ entries = args.flatten.compact.map{|arg| realpath("#{ arg }")}.uniq.sort
379
+
380
+ entries.each do |entry|
381
+ case
382
+ when test(?f, entry)
383
+ file = realpath(entry)
384
+ accum << file
385
+
386
+ when test(?d, entry)
387
+ glob = File.join(entry, '**/**')
388
+
389
+ Dir.glob(glob) do |_entry|
390
+ case
391
+ when test(?f, _entry)
392
+ filelist(_entry){ accum }
393
+ when test(?d, entry)
394
+ filelist(_entry){ accum }
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ accum.to_a
401
+ end
402
+
403
+ def slug_for(*args, &block)
404
+ Slug.for(*args, &block)
405
+ end
406
+
407
+ extend Util
408
+ end
409
+
410
+ def self.utils(&block)
411
+ block ? Util.module_eval(&block) : Util
412
+ end
413
+
414
+ def utils
415
+ Util
416
+ end
417
+
418
+ def u
419
+ Util
420
+ end
421
+
422
+ #
423
+ class Slug < ::String
424
+ Join = '-'
425
+
426
+ def Slug.for(*args)
427
+ options = args.last.is_a?(Hash) ? args.pop : {}
428
+
429
+ join = (options[:join] || options['join'] || Join).to_s
430
+
431
+ string = args.flatten.compact.join(' ')
432
+
433
+ tokens = string.scan(%r`[^\s#{ join }]+`)
434
+
435
+ tokens.map! do |token|
436
+ token.gsub(%r`[^\p{L}/.]`, '').downcase
437
+ end
438
+
439
+ tokens.map! do |token|
440
+ token.gsub(%r`[/.]`, join * 2)
441
+ end
442
+
443
+ tokens.join(join)
444
+ end
445
+ end
446
+
447
+ #
448
+ def noop
449
+ ENV['SCRIPT_NOOP'] || ENV['NOOP']
450
+ end
451
+
452
+ alias_method :noop?, :noop
453
+
454
+ #
455
+ def self.help(*args)
456
+ @help ||= nil
457
+
458
+ unless args.empty?
459
+ @help = utils.unindent(args.join)
460
+ end
461
+
462
+ @help
463
+ end
464
+
465
+ def self.run(*args, &block)
466
+ modes =
467
+ if args.empty?
468
+ [nil]
469
+ else
470
+ args
471
+ end
472
+
473
+ modes.each do |mode|
474
+ method_name =
475
+ if mode
476
+ "run_#{ mode }"
477
+ else
478
+ "run"
479
+ end
480
+
481
+ define_method(method_name, &block)
482
+ end
483
+ end
484
+
485
+ #
486
+ def Script.klass_for(&block)
487
+ Class.new(Script) do |klass|
488
+ def klass.name; "Script::Klass__#{ SecureRandom.uuid.to_s.gsub('-', '_') }"; end
489
+ klass.class_eval(&block)
490
+ end
491
+ end
492
+
493
+ def self.run!(*args, &block)
494
+ STDOUT.sync = true
495
+ STDERR.sync = true
496
+
497
+ %w[ PIPE INT ].each{|signal| Signal.trap(signal, "EXIT")}
498
+
499
+ script = (
500
+ source =
501
+ if binding.respond_to?(:source_location)
502
+ File.expand_path(binding.source_location.first)
503
+ else
504
+ File.expand_path(eval('__FILE__', block.binding))
505
+ end
506
+
507
+ root = File.dirname(source)
508
+
509
+ klass = Script.klass_for(&block)
510
+
511
+ instance = klass.new
512
+
513
+ instance.source = source
514
+ instance.root = root
515
+
516
+ instance
517
+ )
518
+
519
+ script.run!(*args)
520
+ end
521
+ end
522
+ end
523
+
524
+ #
525
+ def Senv.script(*args, &block)
526
+ Senv::Script.run!(*args, &block)
527
+ end