rsql 0.2.6 → 0.2.7

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.
@@ -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