rsql 0.2.7 → 0.2.8
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.
- data/LICENSE +19 -0
- data/example.rsqlrc +291 -0
- data/extra/mysql-client-5.1.59-1.tgz +0 -0
- data/lib/rsql.rb +10 -0
- data/lib/rsql/commands.rb +243 -0
- data/lib/rsql/eval_context.rb +737 -0
- data/lib/rsql/mysql_results.rb +470 -0
- data/test/test_commands.rb +85 -0
- data/test/test_eval_context.rb +179 -0
- data/test/test_mysql_results.rb +192 -0
- metadata +17 -10
@@ -0,0 +1,737 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
|
22
|
+
module RSQL
|
23
|
+
|
24
|
+
require 'ostruct'
|
25
|
+
require 'time'
|
26
|
+
|
27
|
+
# todo: add a simple way to interpolate directly within a sql query about to
|
28
|
+
# be exec'd (so we can save stuff from other queries in variables that can
|
29
|
+
# then be ref'd in a new query all on the cmd line)
|
30
|
+
|
31
|
+
################################################################################
|
32
|
+
# This class wraps all dynamic evaluation and serves as the reflection class
|
33
|
+
# for adding methods dynamically.
|
34
|
+
#
|
35
|
+
class EvalContext
|
36
|
+
|
37
|
+
Registration = Struct.new(:name, :args, :bangs, :block, :usage,
|
38
|
+
:desc, :source, :source_fn)
|
39
|
+
|
40
|
+
HEXSTR_LIMIT = 32
|
41
|
+
|
42
|
+
def initialize(options=OpenStruct.new)
|
43
|
+
@opts = options
|
44
|
+
@prompt = nil
|
45
|
+
@verbose = @opts.verbose
|
46
|
+
@hexstr_limit = HEXSTR_LIMIT
|
47
|
+
@results = nil
|
48
|
+
|
49
|
+
@loaded_fns = []
|
50
|
+
@loaded_fns_state = {}
|
51
|
+
@init_registrations = []
|
52
|
+
@bangs = {}
|
53
|
+
@global_bangs = {}
|
54
|
+
|
55
|
+
@registrations = {
|
56
|
+
:version => Registration.new('version', [], {},
|
57
|
+
method(:version),
|
58
|
+
'version',
|
59
|
+
'Version information about RSQL, the client, and the server.'),
|
60
|
+
:help => Registration.new('help', [], {},
|
61
|
+
method(:help),
|
62
|
+
'help',
|
63
|
+
'Show short syntax help.'),
|
64
|
+
:grep => Registration.new('grep', [], {},
|
65
|
+
method(:grep),
|
66
|
+
'grep',
|
67
|
+
'Show results when regular expression matches any part of the content.'),
|
68
|
+
:reload => Registration.new('reload', [], {},
|
69
|
+
method(:reload),
|
70
|
+
'reload',
|
71
|
+
'Reload the rsqlrc file.'),
|
72
|
+
:desc => Registration.new('desc', [], {},
|
73
|
+
method(:desc),
|
74
|
+
'desc',
|
75
|
+
'Describe the content of a recipe.'),
|
76
|
+
:history => Registration.new('history', [], {},
|
77
|
+
method(:history),
|
78
|
+
'history(cnt=1)',
|
79
|
+
'Print recent queries made (request a count or use :all for entire list).'),
|
80
|
+
:set_max_rows => Registration.new('set_max_rows', [], {},
|
81
|
+
Proc.new{|r| MySQLResults.max_rows = r},
|
82
|
+
'set_max_rows',
|
83
|
+
'Set the maximum number of rows to process.'),
|
84
|
+
:max_rows => Registration.new('max_rows', [], {},
|
85
|
+
Proc.new{MySQLResults.max_rows},
|
86
|
+
'max_rows',
|
87
|
+
'Get the maximum number of rows to process.'),
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :prompt
|
92
|
+
attr_accessor :bangs, :verbose
|
93
|
+
|
94
|
+
def call_init_registrations
|
95
|
+
@init_registrations.each do |sym|
|
96
|
+
reg = @registrations[sym]
|
97
|
+
sql = reg.block.call(*reg.args)
|
98
|
+
query(sql) if String === sql
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def load(fn, opt=nil)
|
103
|
+
@loaded_fns << fn unless @loaded_fns_state.key?(fn)
|
104
|
+
@loaded_fns_state[fn] = :loading
|
105
|
+
|
106
|
+
# this should only be done after we have established a
|
107
|
+
# mysql connection, so this option allows rsql to load the
|
108
|
+
# init file immediately and then later make the init
|
109
|
+
# registration calls--we set this as an instance variable
|
110
|
+
# to allow for loaded files to call load again and yet
|
111
|
+
# still maintain the skip logic
|
112
|
+
if opt == :skip_init_registrations
|
113
|
+
reset_skipping = @skipping_init_registrations = true
|
114
|
+
end
|
115
|
+
|
116
|
+
ret = Thread.new {
|
117
|
+
begin
|
118
|
+
eval(File.read(fn), binding, fn)
|
119
|
+
nil
|
120
|
+
rescue Exception => ex
|
121
|
+
ex
|
122
|
+
end
|
123
|
+
}.value
|
124
|
+
|
125
|
+
if Exception === ret
|
126
|
+
@loaded_fns_state[fn] = :failed
|
127
|
+
if @verbose
|
128
|
+
$stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
|
129
|
+
else
|
130
|
+
bt = ret.backtrace.collect{|line| line.start_with?(fn) ? line : nil}.compact
|
131
|
+
$stderr.puts("#{ret.class}: #{ret.message}", bt, '')
|
132
|
+
end
|
133
|
+
ret = false
|
134
|
+
else
|
135
|
+
@loaded_fns_state[fn] = :loaded
|
136
|
+
call_init_registrations unless @skipping_init_registrations
|
137
|
+
ret = true
|
138
|
+
end
|
139
|
+
|
140
|
+
@skipping_init_registrations = false if reset_skipping
|
141
|
+
|
142
|
+
return ret
|
143
|
+
end
|
144
|
+
|
145
|
+
def reload
|
146
|
+
# some files may be loaded by other files, if so, we don't want to
|
147
|
+
# reload them again here
|
148
|
+
@loaded_fns.each{|fn| @loaded_fns_state[fn] = nil}
|
149
|
+
@loaded_fns.each{|fn| self.load(fn, :skip_init_registrations) if @loaded_fns_state[fn] == nil}
|
150
|
+
|
151
|
+
# load up the inits after all the normal registrations are ready
|
152
|
+
call_init_registrations
|
153
|
+
|
154
|
+
# report all the successfully loaded ones
|
155
|
+
loaded = []
|
156
|
+
@loaded_fns.each{|fn,state| loaded << fn if @loaded_fns_state[fn] == :loaded}
|
157
|
+
puts "loaded: #{loaded.inspect}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def bang_eval(field, val)
|
161
|
+
# allow individual bangs to override global ones, even if they're nil
|
162
|
+
if @bangs.key?(field)
|
163
|
+
bang = @bangs[field]
|
164
|
+
else
|
165
|
+
# todo: this will run on *every* value--this should be optimized
|
166
|
+
# so that it's only run once on each query's result column
|
167
|
+
# fields and then we'd know if any bangs are usable and pased in
|
168
|
+
# for each result value
|
169
|
+
@global_bangs.each do |m,b|
|
170
|
+
if (String === m && m == field.to_s) ||
|
171
|
+
(Regexp === m && m.match(field.to_s))
|
172
|
+
bang = b
|
173
|
+
break
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
if bang
|
179
|
+
begin
|
180
|
+
val = Thread.new{ eval("#{bang}(val)") }.value
|
181
|
+
rescue Exception => ex
|
182
|
+
if @verbose
|
183
|
+
$stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
|
184
|
+
else
|
185
|
+
$stderr.puts(ex.message, ex.backtrace.first)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
return val
|
191
|
+
end
|
192
|
+
|
193
|
+
# Safely evaluate Ruby content within our context.
|
194
|
+
#
|
195
|
+
def safe_eval(content, results, stdout)
|
196
|
+
@results = results
|
197
|
+
|
198
|
+
# allow a simple reload to be called directly as it requires a
|
199
|
+
# little looser safety valve...
|
200
|
+
if 'reload' == content
|
201
|
+
reload
|
202
|
+
return
|
203
|
+
end
|
204
|
+
|
205
|
+
# same relaxed call to load too
|
206
|
+
if m = content.match(/^\s*load\s+'(.+)'\s*$/)
|
207
|
+
self.load(m[1])
|
208
|
+
return
|
209
|
+
end
|
210
|
+
|
211
|
+
# help out the poor user and fix up any describes
|
212
|
+
# requested so they don't need to remember that it needs
|
213
|
+
# to be a symbol passed in
|
214
|
+
if m = content.match(/^\s*desc\s+([^:]\S+)\s*$/)
|
215
|
+
content = "desc :#{m[1]}"
|
216
|
+
end
|
217
|
+
|
218
|
+
if stdout
|
219
|
+
# capture stdout
|
220
|
+
orig_stdout = $stdout
|
221
|
+
$stdout = stdout
|
222
|
+
end
|
223
|
+
|
224
|
+
begin
|
225
|
+
# in order to print out errors in a loaded script so
|
226
|
+
# that we have file/line info, we need to rescue their
|
227
|
+
# exceptions inside the evaluation
|
228
|
+
th = Thread.new do
|
229
|
+
eval('begin;' << content << %q{
|
230
|
+
rescue Exception => ex
|
231
|
+
if @verbose
|
232
|
+
$stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
|
233
|
+
else
|
234
|
+
bt = []
|
235
|
+
ex.backtrace.each do |t|
|
236
|
+
break if t.include?('bin/rsql')
|
237
|
+
bt << t unless t.include?('lib/rsql/') || t.include?('(eval)')
|
238
|
+
end
|
239
|
+
$stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''),bt)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
})
|
243
|
+
end
|
244
|
+
value = th.value
|
245
|
+
rescue Exception => ex
|
246
|
+
$stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
|
247
|
+
ensure
|
248
|
+
$stdout = orig_stdout if stdout
|
249
|
+
end
|
250
|
+
|
251
|
+
return value
|
252
|
+
end
|
253
|
+
|
254
|
+
# Provide a list of tab completions given the prompted value.
|
255
|
+
#
|
256
|
+
def complete(str)
|
257
|
+
if str[0] == ?.
|
258
|
+
str.slice!(0)
|
259
|
+
prefix = '.'
|
260
|
+
else
|
261
|
+
prefix = ''
|
262
|
+
end
|
263
|
+
|
264
|
+
ret = MySQLResults.complete(str)
|
265
|
+
|
266
|
+
ret += @registrations.keys.sort_by{|sym|sym.to_s}.collect do |sym|
|
267
|
+
name = sym.to_s
|
268
|
+
if name.start_with?(str)
|
269
|
+
prefix + name
|
270
|
+
else
|
271
|
+
nil
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
ret.compact!
|
276
|
+
ret
|
277
|
+
end
|
278
|
+
|
279
|
+
# Reset the hexstr limit back to the default value.
|
280
|
+
#
|
281
|
+
def reset_hexstr_limit
|
282
|
+
@hexstr_limit = HEXSTR_LIMIT
|
283
|
+
end
|
284
|
+
|
285
|
+
# Convert a binary string value into a hexadecimal string.
|
286
|
+
#
|
287
|
+
def to_hexstr(bin, limit=@hexstr_limit, prefix='0x')
|
288
|
+
return bin if bin.nil?
|
289
|
+
|
290
|
+
cnt = 0
|
291
|
+
str = prefix << bin.gsub(/./m) do |ch|
|
292
|
+
if limit
|
293
|
+
if limit < 1
|
294
|
+
cnt += 1
|
295
|
+
next
|
296
|
+
end
|
297
|
+
limit -= 1
|
298
|
+
end
|
299
|
+
'%02x' % ch.bytes.first
|
300
|
+
end
|
301
|
+
|
302
|
+
if limit && limit < 1 && 0 < cnt
|
303
|
+
str << "... (#{cnt} bytes hidden)"
|
304
|
+
end
|
305
|
+
|
306
|
+
return str
|
307
|
+
end
|
308
|
+
|
309
|
+
########################################
|
310
|
+
private
|
311
|
+
|
312
|
+
# Display a listing of all registered helpers.
|
313
|
+
#
|
314
|
+
def list # :doc:
|
315
|
+
usagelen = 0
|
316
|
+
desclen = 0
|
317
|
+
|
318
|
+
sorted = @registrations.values.sort_by do |reg|
|
319
|
+
usagelen = reg.usage.length if usagelen < reg.usage.length
|
320
|
+
longest_line = reg.desc.split(/\r?\n/).collect{|l|l.length}.max
|
321
|
+
desclen = longest_line if longest_line && desclen < longest_line
|
322
|
+
reg.usage
|
323
|
+
end
|
324
|
+
|
325
|
+
fmt = "%-#{usagelen}s %s#{$/}"
|
326
|
+
|
327
|
+
printf(fmt, 'usage', 'description')
|
328
|
+
puts '-'*(usagelen+2+desclen)
|
329
|
+
|
330
|
+
sorted.each do |reg|
|
331
|
+
printf(fmt, reg.usage, reg.desc)
|
332
|
+
end
|
333
|
+
|
334
|
+
return nil
|
335
|
+
end
|
336
|
+
|
337
|
+
# Attempt to locate the parameters of a given block by
|
338
|
+
# searching its source.
|
339
|
+
#
|
340
|
+
def params(name, block)
|
341
|
+
params = ''
|
342
|
+
|
343
|
+
if block.arity != 0 && block.arity != -1 &&
|
344
|
+
block.inspect.match(/@(.+):(\d+)>$/)
|
345
|
+
fn = $1
|
346
|
+
lineno = $2.to_i
|
347
|
+
|
348
|
+
if fn == '(eval)'
|
349
|
+
$stderr.puts "refusing to search an eval block for :#{name}"
|
350
|
+
return params
|
351
|
+
end
|
352
|
+
|
353
|
+
File.open(fn) do |f|
|
354
|
+
i = 0
|
355
|
+
found = false
|
356
|
+
while line = f.gets
|
357
|
+
i += 1
|
358
|
+
next if i < lineno
|
359
|
+
|
360
|
+
unless found
|
361
|
+
# give up if no start found within 20
|
362
|
+
# lines
|
363
|
+
break if lineno + 20 < i
|
364
|
+
if m = line.match(/(\{|do\b)(.*)$/)
|
365
|
+
# adjust line to be the remainder
|
366
|
+
# after the start
|
367
|
+
line = m[2]
|
368
|
+
found = true
|
369
|
+
else
|
370
|
+
next
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
if m = line.match(/^\s*\|([^\|]*)\|/)
|
375
|
+
params = "(#{m[1]})"
|
376
|
+
break
|
377
|
+
end
|
378
|
+
|
379
|
+
# if the params aren't here then we'd
|
380
|
+
# better only have whitespace otherwise
|
381
|
+
# this block doesn't have params...even
|
382
|
+
# though arity says it should
|
383
|
+
next if line.match(/^\s*$/)
|
384
|
+
$stderr.puts "unable to locate params for :#{name}"
|
385
|
+
break
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
return params
|
391
|
+
end
|
392
|
+
|
393
|
+
# Similiar to the MySQL "desc" command, show the content
|
394
|
+
# of nearly any registered recipe including where it was
|
395
|
+
# sourced (e.g. what file:line it came from).
|
396
|
+
#
|
397
|
+
def desc(sym)
|
398
|
+
unless Symbol === sym
|
399
|
+
$stderr.puts("must provide a Symbol--try prefixing it with a colon (:)")
|
400
|
+
return
|
401
|
+
end
|
402
|
+
|
403
|
+
unless reg = @registrations[sym]
|
404
|
+
$stderr.puts "nothing registered as #{sym}"
|
405
|
+
return
|
406
|
+
end
|
407
|
+
|
408
|
+
if Method === reg.block
|
409
|
+
$stderr.puts "refusing to describe the #{sym} method"
|
410
|
+
return
|
411
|
+
end
|
412
|
+
|
413
|
+
if !reg.source && reg.block.inspect.match(/@(.+):(\d+)>$/)
|
414
|
+
fn = $1
|
415
|
+
lineno = $2.to_i
|
416
|
+
|
417
|
+
if fn == __FILE__
|
418
|
+
$stderr.puts "refusing to describe EvalContext##{sym}"
|
419
|
+
return
|
420
|
+
end
|
421
|
+
|
422
|
+
if fn == '(eval)'
|
423
|
+
$stderr.puts 'unable to describe body for an eval block'
|
424
|
+
return
|
425
|
+
end
|
426
|
+
|
427
|
+
reg.source_fn = "#{fn}:#{lineno}"
|
428
|
+
|
429
|
+
File.open(fn) do |f|
|
430
|
+
source = ''
|
431
|
+
i = 0
|
432
|
+
ending = nil
|
433
|
+
found = false
|
434
|
+
|
435
|
+
while line = f.gets
|
436
|
+
i += 1
|
437
|
+
next unless ending || i == lineno
|
438
|
+
source << line
|
439
|
+
unless ending
|
440
|
+
unless m = line.match(/\{|do\b/)
|
441
|
+
$stderr.puts "unable to locate block beginning at #{fn}:#{lineno}"
|
442
|
+
return
|
443
|
+
end
|
444
|
+
ending = m[0] == '{' ? '\}' : 'end'
|
445
|
+
next
|
446
|
+
end
|
447
|
+
|
448
|
+
if m = line.match(/^#{ending}/)
|
449
|
+
found = true
|
450
|
+
break
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
if found
|
455
|
+
reg.source = source
|
456
|
+
else
|
457
|
+
reg.source = ''
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
if reg.source && !reg.source.empty?
|
463
|
+
puts '', "[#{reg.source_fn}]", '', reg.source
|
464
|
+
else
|
465
|
+
$stderr.puts "unable to locate body for #{sym}"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Show all the pertinent version data we have about our
|
470
|
+
# software and the mysql connection.
|
471
|
+
#
|
472
|
+
def version # :doc:
|
473
|
+
puts "rsql:v#{RSQL::VERSION} client:v#{MySQLResults.conn.client_info} " \
|
474
|
+
"server:v#{MySQLResults.conn.server_info}"
|
475
|
+
end
|
476
|
+
|
477
|
+
# Show a short amount of information about acceptable syntax.
|
478
|
+
#
|
479
|
+
def help # :doc:
|
480
|
+
puts <<EOF
|
481
|
+
|
482
|
+
Converting values on the fly:
|
483
|
+
|
484
|
+
rsql> select name, value from rsql_example ! value => humanize_bytes;
|
485
|
+
|
486
|
+
Inspect MySQL connection:
|
487
|
+
|
488
|
+
rsql> . p [host_info, proto_info];
|
489
|
+
|
490
|
+
Escape strings:
|
491
|
+
|
492
|
+
rsql> . p escape_string('drop table "here"');
|
493
|
+
|
494
|
+
Show only rows containing a string:
|
495
|
+
|
496
|
+
rsql> select * from rsql_example | grep 'mystuff';
|
497
|
+
|
498
|
+
Show only rows containing a regular expression with case insensitive search:
|
499
|
+
|
500
|
+
rsql> select * from rsql_example | grep /mystuff/i;
|
501
|
+
|
502
|
+
EOF
|
503
|
+
end
|
504
|
+
|
505
|
+
# Provide a helper utility in the event a registered method would
|
506
|
+
# like to make its own queries.
|
507
|
+
#
|
508
|
+
def query(content, *args) # :doc:
|
509
|
+
MySQLResults.query(content, self, *args)
|
510
|
+
end
|
511
|
+
|
512
|
+
# Show the most recent queries made to the MySQL server in this
|
513
|
+
# session. Default is to show the last one.
|
514
|
+
#
|
515
|
+
def history(cnt=1)
|
516
|
+
if h = MySQLResults.history(cnt)
|
517
|
+
h.each{|q| puts '', q}
|
518
|
+
end
|
519
|
+
nil
|
520
|
+
end
|
521
|
+
|
522
|
+
# Call MySQLResults' grep to remove (or show) only those lines that
|
523
|
+
# have content matching the patterrn.
|
524
|
+
#
|
525
|
+
def grep(*args)
|
526
|
+
if @results.grep(*args)
|
527
|
+
@results
|
528
|
+
else
|
529
|
+
$stderr.puts 'No matches found'
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Register bangs to evaluate on all displayers as long as a column
|
534
|
+
# match is located. Bang keys may be either exact string matches or
|
535
|
+
# regular expressions.
|
536
|
+
#
|
537
|
+
def register_global_bangs(bangs)
|
538
|
+
@global_bangs.merge!(bangs)
|
539
|
+
end
|
540
|
+
|
541
|
+
# Exactly like register below except in addition to registering as
|
542
|
+
# a usable call for later, we will also use these as soon as we
|
543
|
+
# have a connection to MySQL.
|
544
|
+
#
|
545
|
+
def register_init(sym, *args, &block) # :doc:
|
546
|
+
register(sym, *args, &block)
|
547
|
+
@init_registrations << sym unless @init_registrations.include?(sym)
|
548
|
+
end
|
549
|
+
|
550
|
+
# If given a block, allow the block to be called later, otherwise,
|
551
|
+
# create a method whose sole purpose is to dynmaically generate
|
552
|
+
# sql with variable interpolation.
|
553
|
+
#
|
554
|
+
def register(sym, *args, &block) # :doc:
|
555
|
+
if m = caller.first.match(/^([^:]+:\d+)/)
|
556
|
+
source_fn = m[1]
|
557
|
+
end
|
558
|
+
|
559
|
+
name = usage = sym.to_s
|
560
|
+
|
561
|
+
if Hash === args.last
|
562
|
+
bangs = args.pop
|
563
|
+
desc = bangs.delete(:desc)
|
564
|
+
else
|
565
|
+
bangs = {}
|
566
|
+
end
|
567
|
+
|
568
|
+
desc = '' unless desc
|
569
|
+
|
570
|
+
if block.nil?
|
571
|
+
source = args.pop.strip
|
572
|
+
sql = squeeze!(source.dup)
|
573
|
+
|
574
|
+
argstr = args.join(',')
|
575
|
+
usage << "(#{argstr})" unless argstr.empty?
|
576
|
+
|
577
|
+
blockstr = %{lambda{|#{argstr}|%{#{sql}} % [#{argstr}]}}
|
578
|
+
block = Thread.new{ eval(blockstr) }.value
|
579
|
+
args = []
|
580
|
+
else
|
581
|
+
source = nil
|
582
|
+
usage << params(name, block)
|
583
|
+
end
|
584
|
+
|
585
|
+
@registrations[sym] = Registration.new(name, args, bangs, block, usage,
|
586
|
+
desc, source, source_fn)
|
587
|
+
end
|
588
|
+
|
589
|
+
# Convert a list of values into a comma-delimited string,
|
590
|
+
# optionally with each value in single quotes.
|
591
|
+
#
|
592
|
+
def to_list(vals, quoted=false) # :doc:
|
593
|
+
vals.collect{|v| quoted ? "'#{v}'" : v.to_s}.join(',')
|
594
|
+
end
|
595
|
+
|
596
|
+
# Convert a collection of values into hexadecimal strings.
|
597
|
+
#
|
598
|
+
def hexify(*ids) # :doc:
|
599
|
+
ids.collect do |id|
|
600
|
+
case id
|
601
|
+
when String
|
602
|
+
if id.start_with?('0x')
|
603
|
+
id
|
604
|
+
else
|
605
|
+
'0x' << id
|
606
|
+
end
|
607
|
+
when Integer
|
608
|
+
'0x' << id.to_s(16)
|
609
|
+
else
|
610
|
+
raise "invalid id: #{id.class}"
|
611
|
+
end
|
612
|
+
end.join(',')
|
613
|
+
end
|
614
|
+
|
615
|
+
# Convert a number of bytes into a human readable string.
|
616
|
+
#
|
617
|
+
def humanize_bytes(bytes) # :doc:
|
618
|
+
abbrev = ['B ','KB','MB','GB','TB','PB','EB','ZB','YB']
|
619
|
+
bytes = bytes.to_i
|
620
|
+
fmt = '%7.2f'
|
621
|
+
|
622
|
+
abbrev.each_with_index do |a,i|
|
623
|
+
if bytes < (1024**(i+1))
|
624
|
+
if i == 0
|
625
|
+
return "#{fmt % bytes} B"
|
626
|
+
else
|
627
|
+
b = bytes / (1024.0**i)
|
628
|
+
return "#{fmt % b} #{a}"
|
629
|
+
end
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
return bytes.to_s
|
634
|
+
end
|
635
|
+
|
636
|
+
# Convert a human readable string of bytes into an integer.
|
637
|
+
#
|
638
|
+
def dehumanize_bytes(str) # :doc:
|
639
|
+
abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
|
640
|
+
|
641
|
+
if str =~ /(\d+(\.\d+)?)\s*(\w+)?/
|
642
|
+
b = $1.to_f
|
643
|
+
if $3
|
644
|
+
i = abbrev.index($3.upcase)
|
645
|
+
return (b * (1024**i)).round
|
646
|
+
else
|
647
|
+
return b.round
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
raise "unable to parse '#{str}'"
|
652
|
+
end
|
653
|
+
|
654
|
+
# Show a nice percent value of a decimal string.
|
655
|
+
#
|
656
|
+
def humanize_percentage(decimal, precision=1) # :doc:
|
657
|
+
if decimal.nil? || decimal == 'NULL'
|
658
|
+
'NA'
|
659
|
+
else
|
660
|
+
"%5.#{precision}f%%" % (decimal.to_f * 100)
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
# Convert a time into a relative string from now.
|
665
|
+
#
|
666
|
+
def relative_time(dt) # :doc:
|
667
|
+
return dt unless String === dt
|
668
|
+
|
669
|
+
now = Time.now.utc
|
670
|
+
theirs = Time.parse(dt + ' UTC')
|
671
|
+
if theirs < now
|
672
|
+
diff = now - theirs
|
673
|
+
postfix = 'ago'
|
674
|
+
else
|
675
|
+
diff = theirs - now
|
676
|
+
postfix = 'from now'
|
677
|
+
end
|
678
|
+
|
679
|
+
fmt = '%3.0f'
|
680
|
+
|
681
|
+
[
|
682
|
+
[31556926.0, 'years'],
|
683
|
+
[2629743.83, 'months'],
|
684
|
+
[86400.0, 'days'],
|
685
|
+
[3600.0, 'hours'],
|
686
|
+
[60.0, 'minutes']
|
687
|
+
].each do |(limit, label)|
|
688
|
+
if (limit * 1.5) < diff
|
689
|
+
return "#{fmt % (diff / limit)} #{label} #{postfix}"
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
return "#{fmt % diff} seconds #{postfix}"
|
694
|
+
end
|
695
|
+
|
696
|
+
# Squeeze out any spaces.
|
697
|
+
#
|
698
|
+
def squeeze!(sql) # :doc:
|
699
|
+
sql.gsub!(/\s+/,' ')
|
700
|
+
sql.strip!
|
701
|
+
sql << ';' unless sql[-1] == ?;
|
702
|
+
sql
|
703
|
+
end
|
704
|
+
|
705
|
+
# Safely store an object into a file keeping at most one
|
706
|
+
# backup if the file already exists.
|
707
|
+
#
|
708
|
+
def safe_save(obj, name) # :doc:
|
709
|
+
name += '.yml' unless File.extname(name) == '.yml'
|
710
|
+
tn = "#{name}.tmp"
|
711
|
+
File.open(tn, 'w'){|f| YAML.dump(obj, f)}
|
712
|
+
if File.exist?(name)
|
713
|
+
bn = "#{name}~"
|
714
|
+
File.unlink(bn) if File.exist?(bn)
|
715
|
+
File.rename(name, bn)
|
716
|
+
end
|
717
|
+
File.rename(tn, name)
|
718
|
+
puts "Saved: #{name}"
|
719
|
+
end
|
720
|
+
|
721
|
+
def method_missing(sym, *args, &block)
|
722
|
+
if reg = @registrations[sym]
|
723
|
+
@bangs.merge!(reg.bangs)
|
724
|
+
final_args = reg.args + args
|
725
|
+
reg.block.call(*final_args)
|
726
|
+
elsif MySQLResults.respond_to?(sym)
|
727
|
+
MySQLResults.send(sym, *args)
|
728
|
+
elsif MySQLResults.conn.respond_to?(sym)
|
729
|
+
MySQLResults.conn.send(sym, *args)
|
730
|
+
else
|
731
|
+
super.method_missing(sym, *args, &block)
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
end # class EvalContext
|
736
|
+
|
737
|
+
end # module RSQL
|