rsql 0.2.6 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/rsql +81 -57
- metadata +6 -16
- data/LICENSE +0 -19
- data/example.rsqlrc +0 -291
- data/extra/mysql-client-5.1.59-1.tgz +0 -0
- data/lib/rsql.rb +0 -10
- data/lib/rsql/commands.rb +0 -243
- data/lib/rsql/eval_context.rb +0 -690
- data/lib/rsql/mysql_results.rb +0 -410
- data/test/test_commands.rb +0 -85
- data/test/test_eval_context.rb +0 -179
- data/test/test_mysql_results.rb +0 -130
Binary file
|
data/lib/rsql.rb
DELETED
data/lib/rsql/commands.rb
DELETED
@@ -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
|
data/lib/rsql/eval_context.rb
DELETED
@@ -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
|