rsql 0.2.6 → 0.2.7

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