Class: RSQL::EvalContext

Inherits:
Object
  • Object
show all
Defined in:
lib/rsql/eval_context.rb

Overview

This class wraps all dynamic evaluation and serves as the reflection class for adding methods dynamically.

Defined Under Namespace

Classes: Registration

Constant Summary collapse

HEXSTR_LIMIT =
32

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = OpenStruct.new) ⇒ EvalContext

Returns a new instance of EvalContext.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rsql/eval_context.rb', line 42

def initialize(options=OpenStruct.new)
    @opts         = options
    @prompt       = nil
    @verbose      = @opts.verbose
    @hexstr_limit = HEXSTR_LIMIT
    @results      = nil

    @loaded_fns         = []
    @loaded_fns_state   = {}
    @init_registrations = []
    @bangs              = {}
    @global_bangs       = {}

    @registrations = {
        :version => Registration.new('version', [], {},
            method(:version),
            'version',
            'Version information about RSQL, the client, and the server.'),
        :help => Registration.new('help', [], {},
            method(:help),
            'help',
            'Show short syntax help.'),
        :grep => Registration.new('grep', [], {},
            method(:grep),
            'grep(string_or_regexp, *options)',
            'Show results when regular expression matches any part of the content.'),
        :reload => Registration.new('reload', [], {},
            method(:reload),
            'reload',
            'Reload the rsqlrc file.'),
        :desc => Registration.new('desc', [], {},
            method(:desc),
            'desc(name)',
            'Describe the content of a recipe.'),
        :history => Registration.new('history', [], {},
            method(:history),
            'history(cnt=1)',
            'Print recent queries made (request a count or use :all for entire list).'),
        :set_max_rows => Registration.new('set_max_rows', [], {},
            Proc.new{|r| MySQLResults.max_rows = r},
            'set_max_rows(max)',
            'Set the maximum number of rows to process.'),
        :max_rows => Registration.new('max_rows', [], {},
            Proc.new{MySQLResults.max_rows},
            'max_rows',
            'Get the maximum number of rows to process.'),
    }
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object (private)



757
758
759
760
761
762
763
764
765
766
767
768
769
# File 'lib/rsql/eval_context.rb', line 757

def method_missing(sym, *args, &block)
    if reg = @registrations[sym]
        @bangs.merge!(reg.bangs)
        final_args = reg.args + args
        reg.block.call(*final_args)
    elsif MySQLResults.respond_to?(sym)
        MySQLResults.send(sym, *args)
    elsif MySQLResults.conn.respond_to?(sym)
        MySQLResults.conn.send(sym, *args)
    else
        super.method_missing(sym, *args, &block)
    end
end

Instance Attribute Details

#bangsObject

Returns the value of attribute bangs.



92
93
94
# File 'lib/rsql/eval_context.rb', line 92

def bangs
  @bangs
end

#promptObject (readonly)

Returns the value of attribute prompt.



91
92
93
# File 'lib/rsql/eval_context.rb', line 91

def prompt
  @prompt
end

#verboseObject

Returns the value of attribute verbose.



92
93
94
# File 'lib/rsql/eval_context.rb', line 92

def verbose
  @verbose
end

Instance Method Details

#bang_eval(field, val) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rsql/eval_context.rb', line 160

def bang_eval(field, val)
    # allow individual bangs to override global ones, even if they're nil
    if @bangs.key?(field)
        bang = @bangs[field]
    else
        # todo: this will run on *every* value--this should be optimized
        # so that it's only run once on each query's result column
        # fields and then we'd know if any bangs are usable and pased in
        # for each result value
        @global_bangs.each do |m,b|
            if (String === m && m == field.to_s) ||
                (Regexp === m && m.match(field.to_s))
                bang = b
                break
            end
        end
    end

    if bang
        begin
            val = Thread.new{ eval("#{bang}(val)") }.value
        rescue Exception => ex
            if @verbose
                $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
            else
                $stderr.puts(ex.message, ex.backtrace.first)
            end
        end
    end

    return val
end

#call_init_registrationsObject



94
95
96
97
98
99
100
# File 'lib/rsql/eval_context.rb', line 94

def call_init_registrations
    @init_registrations.each do |sym|
        reg = @registrations[sym]
        sql = reg.block.call(*reg.args)
        query(sql) if String === sql
    end
end

#complete(str) ⇒ Object

Provide a list of tab completions given the prompted value.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/rsql/eval_context.rb', line 256

def complete(str)
    if str[0] == ?.
        str.slice!(0)
        prefix = '.'
    else
        prefix = ''
    end

    ret  = MySQLResults.complete(str)

    ret += @registrations.keys.sort_by{|sym|sym.to_s}.collect do |sym|
        name = sym.to_s
        if name.start_with?(str)
            prefix + name
        else
            nil
        end
    end

    ret.compact!
    ret
end

#dehumanize_bytes(str) ⇒ Object (private)

Convert a human readable string of bytes into an integer.



674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
# File 'lib/rsql/eval_context.rb', line 674

def dehumanize_bytes(str) # :doc:
    abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']

    if str =~ /(\d+(\.\d+)?)\s*(\w+)?/
        b = $1.to_f
        if $3
            i = abbrev.index($3.upcase)
            return (b * (1024**i)).round
        else
            return b.round
        end
    end

    raise "unable to parse '#{str}'"
end

#desc(sym) ⇒ Object (private)

Similiar to the MySQL “desc” command, show the content of nearly any registered recipe including where it was sourced (e.g. what file:line it came from).



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/rsql/eval_context.rb', line 400

def desc(sym)
    unless Symbol === sym
        $stderr.puts("must provide a Symbol--try prefixing it with a colon (:)")
        return
    end

    unless reg = @registrations[sym]
        $stderr.puts "nothing registered as #{sym}"
        return
    end

    if Method === reg.block
        $stderr.puts "refusing to describe the #{sym} method"
        return
    end

    if !reg.source && reg.block.inspect.match(/@(.+):(\d+)>$/)
        fn = $1
        lineno = $2.to_i

        if fn == __FILE__
            $stderr.puts "refusing to describe EvalContext##{sym}"
            return
        end

        if fn == '(eval)'
            $stderr.puts 'unable to describe body for an eval block'
            return
        end

        reg.source_fn = "#{fn}:#{lineno}"

        File.open(fn) do |f|
            source = ''
            ending = ''

            locate_block_start(sym, f, lineno, ending, source)
            break if ending.empty?

            while line = f.gets
                source << line
                if m = line.match(/^#{ending}/)
                    found = true
                    break
                end
            end

            if found
                reg.source = source
            else
                reg.source = ''
            end
        end
    end

    if reg.source && !reg.source.empty?
        puts '', "[#{reg.source_fn}]", '', reg.source
    else
        $stderr.puts "unable to locate body for #{sym}"
    end
end

#grep(pattern, *gopts) ⇒ Object (private)

Remove all rows that do NOT match the expression. Returns true if any matches were found.

Options:

:fixed   => indicates that the string should be escaped of any
            special characters
:nocolor => will not add color escape codes to indicate the
            match
:inverse => reverses the regular expression match


526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/rsql/eval_context.rb', line 526

def grep(pattern, *gopts) # :doc:
    nocolor = gopts.include?(:nocolor)

    if inverted = gopts.include?(:inverse)
        # there's no point in coloring matches we are removing
        nocolor = true
    end

    if gopts.include?(:fixed)
        regexp = Regexp.new(/#{Regexp.escape(pattern.to_str)}/)
    elsif Regexp === pattern
        regexp = pattern
    else
        regexp = Regexp.new(/#{pattern.to_str}/)
    end

    rval = inverted

    @results.delete_if do |row|
        matched = false
        row.each do |val|
            val = val.to_s unless String === val
            if nocolor
                if matched = !val.match(regexp).nil?
                    rval = inverted ? false : true
                    break
                end
            else
                # in the color case, we want to colorize all hits in
                # all columns, so we can't early terminate our
                # search
                if val.gsub!(regexp){|m| "\e[31;1m#{m}\e[0m"}
                    matched = true
                    rval = inverted ? false : true
                end
            end
        end
        inverted ? matched : !matched
    end

    return rval
end

#helpObject (private)

Show a short amount of information about acceptable syntax.



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/rsql/eval_context.rb', line 472

def help            # :doc:
    puts <<EOF

Converting values on the fly:

  rsql> select name, value from rsql_example ! value => humanize_bytes;

Inspect MySQL connection:

  rsql> . p [host_info, proto_info];

Escape strings:

  rsql> . p escape_string('drop table "here"');

Show only rows containing a string:

  rsql> select * from rsql_example | grep 'mystuff';

Show only rows containing a regular expression with case insensitive search:

  rsql> select * from rsql_example | grep /mystuff/i;

EOF
end

#hexify(*ids) ⇒ Object (private)

Convert a collection of values into hexadecimal strings.



634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# File 'lib/rsql/eval_context.rb', line 634

def hexify(*ids)    # :doc:
    ids.collect do |id|
        case id
        when String
            if id.start_with?('0x')
                id
            else
                '0x' << id
            end
        when Integer
            '0x' << id.to_s(16)
        else
            raise "invalid id: #{id.class}"
        end
    end.join(',')
end

#history(cnt = 1) ⇒ Object (private)

Show the most recent queries made to the MySQL server in this session. Default is to show the last one.



509
510
511
512
513
514
# File 'lib/rsql/eval_context.rb', line 509

def history(cnt=1) # :doc:
    if h = MySQLResults.history(cnt)
        h.each{|q| puts '', q}
    end
    nil
end

#humanize_bytes(bytes) ⇒ Object (private)

Convert a number of bytes into a human readable string.



653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# File 'lib/rsql/eval_context.rb', line 653

def humanize_bytes(bytes) # :doc:
    abbrev = ['B ','KB','MB','GB','TB','PB','EB','ZB','YB']
    bytes = bytes.to_i
    fmt = '%7.2f'

    abbrev.each_with_index do |a,i|
        if bytes < (1024**(i+1))
            if i == 0
                return "#{fmt % bytes} B"
            else
                b = bytes / (1024.0**i)
                return "#{fmt % b} #{a}"
            end
        end
    end

    return bytes.to_s
end

#humanize_percentage(decimal, precision = 1) ⇒ Object (private)

Show a nice percent value of a decimal string.



692
693
694
695
696
697
698
# File 'lib/rsql/eval_context.rb', line 692

def humanize_percentage(decimal, precision=1) # :doc:
    if decimal.nil? || decimal == 'NULL'
        'NA'
    else
        "%5.#{precision}f%%" % (decimal.to_f * 100)
    end
end

#listObject (private)

Display a listing of all registered helpers.



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/rsql/eval_context.rb', line 314

def list            # :doc:
    usagelen = 0
    desclen  = 0

    sorted = @registrations.values.sort_by do |reg|
        usagelen = reg.usage.length if usagelen < reg.usage.length
        longest_line = reg.desc.split(/\r?\n/).collect{|l|l.length}.max
        desclen = longest_line if longest_line && desclen < longest_line
        reg.usage
    end

    fmt = "%-#{usagelen}s  %s#{$/}"

    printf(fmt, 'usage', 'description')
    puts '-'*(usagelen+2+desclen)

    sorted.each do |reg|
        printf(fmt, reg.usage, reg.desc)
    end

    return nil
end

#load(fn, opt = nil) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/rsql/eval_context.rb', line 102

def load(fn, opt=nil)
    @loaded_fns << fn unless @loaded_fns_state.key?(fn)
    @loaded_fns_state[fn] = :loading

    # this should only be done after we have established a
    # mysql connection, so this option allows rsql to load the
    # init file immediately and then later make the init
    # registration calls--we set this as an instance variable
    # to allow for loaded files to call load again and yet
    # still maintain the skip logic
    if opt == :skip_init_registrations
        reset_skipping = @skipping_init_registrations = true
    end

    ret = Thread.new {
        begin
            eval(File.read(fn), binding, fn)
            nil
        rescue Exception => ex
            ex
        end
    }.value

    if Exception === ret
        @loaded_fns_state[fn] = :failed
        if @verbose
            $stderr.puts("#{ret.class}: #{ret.message}", ex.backtrace)
        else
            bt = ret.backtrace.collect{|line| line.start_with?(fn) ? line : nil}.compact
            $stderr.puts("#{ret.class}: #{ret.message}", bt, '')
        end
        ret = false
    else
        @loaded_fns_state[fn] = :loaded
        call_init_registrations unless @skipping_init_registrations
        ret = true
    end

    @skipping_init_registrations = false if reset_skipping

    return ret
end

#locate_block_start(name, io, lineno, ending = nil, source = nil) ⇒ Object (private)

Used by params() and desc() to find where a block begins.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/rsql/eval_context.rb', line 339

def locate_block_start(name, io, lineno, ending=nil, source=nil)
    i = 0
    param_line = ''
    params = nil

    while line = io.gets
        i += 1
        next if i < lineno
        source << line if source

        # give up if no start found within 20 lines
        break if lineno + 20 < i
        if m = line.match(/(\{|do\b)(.*)$/)
            if ending
                ending << (m[1] == '{' ? '\}' : 'end')
            end
            # adjust line to be the remainder after the start
            param_line = m[2]
            break
        end
    end

    if m = param_line.match(/^\s*\|([^\|]*)\|/)
        return "(#{m[1]})"
    else
        return nil
    end
end

#params(name, block) ⇒ Object (private)

Attempt to locate the parameters of a given block by searching its source.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/rsql/eval_context.rb', line 371

def params(name, block)
    params = nil

    if block.arity != 0 && block.inspect.match(/@(.+):(\d+)>$/)
        fn = $1
        lineno = $2.to_i

        if fn == '(eval)'
            $stderr.puts "refusing to search an eval block for :#{name}"
            return ''
        end

        File.open(fn) do |f|
            params = locate_block_start(name, f, lineno)
        end
    end

    if params.nil?
        $stderr.puts "unable to locate params for :#{name}" if @verbose
        return ''
    end

    return params
end

#query(content, *args) ⇒ Object (private)

Provide a helper utility in the event a registered method would like to make its own queries. See MySQLResults.query for more details regarding the other arguments available.



502
503
504
# File 'lib/rsql/eval_context.rb', line 502

def query(content, *args) # :doc:
    MySQLResults.query(content, self, *args)
end

#register(sym, *args, &block) ⇒ Object (private)

If given a block, allow the block to be called later, otherwise, create a method whose sole purpose is to dynmaically generate sql with variable interpolation.



590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/rsql/eval_context.rb', line 590

def register(sym, *args, &block) # :doc:
    if m = caller.first.match(/^([^:]+:\d+)/)
        source_fn = m[1]
    end

    name = usage = sym.to_s

    if Hash === args.last
        bangs = args.pop
        desc = bangs.delete(:desc)
    else
        bangs = {}
    end

    desc = '' unless desc

    if block.nil?
        source = args.pop.strip
        sql = squeeze!(source.dup)

        argstr = args.join(',')
        usage << "(#{argstr})" unless argstr.empty?

        blockstr = %{lambda{|#{argstr}|%{#{sql}} % [#{argstr}]}}
        block = Thread.new{ eval(blockstr) }.value
        args = []
    else
        source = nil
        usage << params(name, block)
    end

    @registrations[sym] = Registration.new(name, args, bangs, block, usage,
                                           desc, source, source_fn)
end

#register_global_bangs(bangs) ⇒ Object (private)

Register bangs to evaluate on all displayers as long as a column match is located. Bang keys may be either exact string matches or regular expressions.



573
574
575
# File 'lib/rsql/eval_context.rb', line 573

def register_global_bangs(bangs)
    @global_bangs.merge!(bangs)
end

#register_init(sym, *args, &block) ⇒ Object (private)

Exactly like register below except in addition to registering as a usable call for later, we will also use these as soon as we have a connection to MySQL.



581
582
583
584
# File 'lib/rsql/eval_context.rb', line 581

def register_init(sym, *args, &block) # :doc:
    register(sym, *args, &block)
    @init_registrations << sym unless @init_registrations.include?(sym)
end

#relative_time(dt) ⇒ Object (private)

Convert a time into a relative string from now.



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/rsql/eval_context.rb', line 702

def relative_time(dt) # :doc:
    return dt unless String === dt

    now = Time.now.utc
    theirs = Time.parse(dt + ' UTC')
    if theirs < now
        diff = now - theirs
        postfix = 'ago'
    else
        diff = theirs - now
        postfix = 'from now'
    end

    fmt = '%3.0f'

    [
     [31556926.0, 'years'],
     [2629743.83, 'months'],
     [86400.0,    'days'],
     [3600.0,     'hours'],
     [60.0,       'minutes']
    ].each do |(limit, label)|
        if (limit * 1.5) < diff
            return "#{fmt % (diff / limit)} #{label} #{postfix}"
        end
    end

    return "#{fmt % diff} seconds #{postfix}"
end

#reloadObject



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rsql/eval_context.rb', line 145

def reload
    # some files may be loaded by other files, if so, we don't want to
    # reload them again here
    @loaded_fns.each{|fn| @loaded_fns_state[fn] = nil}
    @loaded_fns.each{|fn| self.load(fn, :skip_init_registrations) if @loaded_fns_state[fn] == nil}

    # load up the inits after all the normal registrations are ready
    call_init_registrations

    # report all the successfully loaded ones
    loaded = []
    @loaded_fns.each{|fn,state| loaded << fn if @loaded_fns_state[fn] == :loaded}
    puts "loaded: #{loaded.inspect}"
end

#reset_hexstr_limitObject

Reset the hexstr limit back to the default value.



281
282
283
# File 'lib/rsql/eval_context.rb', line 281

def reset_hexstr_limit
    @hexstr_limit = HEXSTR_LIMIT
end

#safe_eval(content, results, stdout) ⇒ Object

Safely evaluate Ruby content within our context.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/rsql/eval_context.rb', line 195

def safe_eval(content, results, stdout)
    @results = results

    # allow a simple reload to be called directly as it requires a
    # little looser safety valve...
    if 'reload' == content
        reload
        return
    end

    # same relaxed call to load too
    if m = content.match(/^\s*load\s+'(.+)'\s*$/)
        self.load(m[1])
        return
    end

    # help out the poor user and fix up any describes
    # requested so they don't need to remember that it needs
    # to be a symbol passed in
    if m = content.match(/^\s*desc\s+([^:]\S+)\s*$/)
        content = "desc :#{m[1]}"
    end

    if stdout
        # capture stdout
        orig_stdout = $stdout
        $stdout = stdout
    end

    begin
        # in order to print out errors in a loaded script so
        # that we have file/line info, we need to rescue their
        # exceptions inside the evaluation
        th = Thread.new do
            eval('begin;' << content << %q{
              rescue Exception => ex
                if @verbose
                    $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
                else
                    bt = []
                    ex.backtrace.each do |t|
                      break if t.include?('bin/rsql')
                      bt << t unless t.include?('lib/rsql/') || t.include?('(eval)')
                    end
                    $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''),bt)
                end
              end
            })
        end
        value = th.value
    rescue Exception => ex
        $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
    ensure
        $stdout = orig_stdout if stdout
    end

    return value
end

#safe_save(obj, name) ⇒ Object (private)

Safely store an object into a file keeping at most one backup if the file already exists.



744
745
746
747
748
749
750
751
752
753
754
755
# File 'lib/rsql/eval_context.rb', line 744

def safe_save(obj, name) # :doc:
    name += '.yml' unless File.extname(name) == '.yml'
    tn = "#{name}.tmp"
    File.open(tn, 'w'){|f| YAML.dump(obj, f)}
    if File.exist?(name)
        bn = "#{name}~"
        File.unlink(bn) if File.exist?(bn)
        File.rename(name, bn)
    end
    File.rename(tn, name)
    puts "Saved: #{name}"
end

#squeeze!(sql) ⇒ Object (private)

Squeeze out any spaces.



734
735
736
737
738
739
# File 'lib/rsql/eval_context.rb', line 734

def squeeze!(sql)    # :doc:
    sql.gsub!(/\s+/,' ')
    sql.strip!
    sql << ';' unless sql[-1] == ?;
    sql
end

#to_hexstr(bin, limit = @hexstr_limit, prefix = '0x') ⇒ Object

Convert a binary string value into a hexadecimal string.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/rsql/eval_context.rb', line 287

def to_hexstr(bin, limit=@hexstr_limit, prefix='0x')
    return bin if bin.nil?

    cnt = 0
    str = prefix << bin.gsub(/./m) do |ch|
        if limit
            if limit < 1
                cnt += 1
                next
            end
            limit -= 1
        end
        '%02x' % ch.bytes.first
    end

    if limit && limit < 1 && 0 < cnt
        str << "... (#{cnt} bytes hidden)"
    end

    return str
end

#to_list(vals, quoted = false) ⇒ Object (private)

Convert a list of values into a comma-delimited string, optionally with each value in single quotes.



628
629
630
# File 'lib/rsql/eval_context.rb', line 628

def to_list(vals, quoted=false) # :doc:
    vals.collect{|v| quoted ? "'#{v}'" : v.to_s}.join(',')
end

#versionObject (private)

Show all the pertinent version data we have about our software and the mysql connection.



465
466
467
468
# File 'lib/rsql/eval_context.rb', line 465

def version         # :doc:
    puts "rsql:v#{RSQL::VERSION} client:v#{MySQLResults.conn.client_info} " \
         "server:v#{MySQLResults.conn.server_info}"
end