rsql 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/README.txt +119 -0
- data/TODO +30 -0
- data/bin/rsql +357 -0
- data/lib/rsql/commands.rb +219 -0
- data/lib/rsql/eval_context.rb +414 -0
- data/lib/rsql/mysql.rb +1127 -0
- data/lib/rsql/mysql_results.rb +320 -0
- data/lib/rsql.rb +8 -0
- metadata +94 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
# Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module RSQL
|
22
|
+
|
23
|
+
require 'stringio'
|
24
|
+
|
25
|
+
EvalResults = Struct.new(:value, :stdout)
|
26
|
+
|
27
|
+
########################################
|
28
|
+
# A wrapper to parse and handle commands
|
29
|
+
#
|
30
|
+
class Commands
|
31
|
+
|
32
|
+
Command = Struct.new(:content, :bangs, :declarator, :displayer)
|
33
|
+
|
34
|
+
########################################
|
35
|
+
|
36
|
+
# split on separators, allowing for escaping
|
37
|
+
#
|
38
|
+
SEPARATORS = ';|!'
|
39
|
+
def initialize(input, default_displayer)
|
40
|
+
@default_displayer = default_displayer
|
41
|
+
@cmds = []
|
42
|
+
esc = ''
|
43
|
+
bangs = {}
|
44
|
+
match_before_bang = nil
|
45
|
+
in_pipe_arg = false
|
46
|
+
next_is_ruby = false
|
47
|
+
|
48
|
+
input.scan(/[^#{SEPARATORS}]+.?/) do |match|
|
49
|
+
if i = SEPARATORS.index(match[-1])
|
50
|
+
sep = SEPARATORS[i]
|
51
|
+
match.chop!
|
52
|
+
|
53
|
+
if match[-1] == ?\\
|
54
|
+
# unescape the separator and save the content away
|
55
|
+
esc << match[0..-2] << sep
|
56
|
+
next
|
57
|
+
end
|
58
|
+
else
|
59
|
+
sep = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
if esc.any?
|
63
|
+
esc << match
|
64
|
+
match = esc
|
65
|
+
esc = ''
|
66
|
+
end
|
67
|
+
|
68
|
+
if match_before_bang
|
69
|
+
new_bangs = {}
|
70
|
+
match.split(/\s*,\s*/).each do |ent|
|
71
|
+
(key,val) = ent.split(/\s*=>\s*/)
|
72
|
+
unless key && val
|
73
|
+
# they are using a bang but have no maps
|
74
|
+
# so we assume this is a != or something
|
75
|
+
# similar and let it go through unmapped
|
76
|
+
esc = match_before_bang + '!' + match
|
77
|
+
match_before_bang = nil
|
78
|
+
break
|
79
|
+
end
|
80
|
+
new_bangs[key.strip] = val.to_sym
|
81
|
+
end
|
82
|
+
next unless match_before_bang
|
83
|
+
match = match_before_bang
|
84
|
+
match_before_bang = nil
|
85
|
+
bangs.merge!(new_bangs)
|
86
|
+
end
|
87
|
+
|
88
|
+
if sep == ?!
|
89
|
+
match_before_bang = match
|
90
|
+
next
|
91
|
+
end
|
92
|
+
|
93
|
+
if sep == ?|
|
94
|
+
# we've split on a pipe so we need to handle the
|
95
|
+
# case where ruby code is declaring a block with
|
96
|
+
# arguments (e.g. {|x| p x} or do |x| p x end)
|
97
|
+
if in_pipe_arg
|
98
|
+
in_pipe_arg = false
|
99
|
+
esc << match << '|'
|
100
|
+
next
|
101
|
+
elsif match =~ /\{\s*|do\s*/
|
102
|
+
in_pipe_arg = true
|
103
|
+
esc << match << '|'
|
104
|
+
next
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
add_command(match, bangs, next_is_ruby, sep)
|
109
|
+
|
110
|
+
bangs = {}
|
111
|
+
next_is_ruby = sep == ?|
|
112
|
+
end
|
113
|
+
|
114
|
+
add_command(esc, bangs, next_is_ruby)
|
115
|
+
end
|
116
|
+
|
117
|
+
def empty?
|
118
|
+
return @cmds.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
def run!(eval_context)
|
122
|
+
last_results = nil
|
123
|
+
while @cmds.any?
|
124
|
+
cmd = @cmds.shift
|
125
|
+
results = run_command(cmd, last_results, eval_context)
|
126
|
+
return :done if results == :done
|
127
|
+
|
128
|
+
if cmd.displayer == :pipe
|
129
|
+
last_results = results
|
130
|
+
elsif MySQLResults === results
|
131
|
+
last_results = nil
|
132
|
+
results.send(cmd.displayer)
|
133
|
+
elsif EvalResults === results
|
134
|
+
last_results = nil
|
135
|
+
if results.stdout && 0 < results.stdout.size
|
136
|
+
puts results.stdout.string
|
137
|
+
end
|
138
|
+
puts "=> #{results.value.inspect}" if results.value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
########################################
|
144
|
+
private
|
145
|
+
|
146
|
+
def add_command(content, bangs, is_ruby, separator=nil)
|
147
|
+
content.strip!
|
148
|
+
|
149
|
+
case content[0]
|
150
|
+
when ?.
|
151
|
+
content.slice!(0)
|
152
|
+
declarator = :ruby
|
153
|
+
when ?@
|
154
|
+
content.slice!(0)
|
155
|
+
declarator = :iterator
|
156
|
+
else
|
157
|
+
declarator = is_ruby ? :ruby : nil
|
158
|
+
end
|
159
|
+
|
160
|
+
if content.end_with?('\G')
|
161
|
+
# emulate mysql's \G output
|
162
|
+
content.slice!(-2,2)
|
163
|
+
displayer = :display_by_line
|
164
|
+
elsif separator == ?|
|
165
|
+
displayer = :pipe
|
166
|
+
else
|
167
|
+
displayer = @default_displayer
|
168
|
+
end
|
169
|
+
|
170
|
+
if content.any?
|
171
|
+
@cmds << Command.new(content, bangs, declarator, displayer)
|
172
|
+
return true
|
173
|
+
end
|
174
|
+
|
175
|
+
return false
|
176
|
+
end
|
177
|
+
|
178
|
+
def run_command(cmd, last_results, eval_context)
|
179
|
+
ctx = EvalContext::CommandContext.new
|
180
|
+
|
181
|
+
# set up to allow an iterator to run up to 100,000 times
|
182
|
+
100000.times do |i|
|
183
|
+
eval_context.bangs = cmd.bangs
|
184
|
+
|
185
|
+
if cmd.declarator
|
186
|
+
ctx.index = i
|
187
|
+
ctx.last_results = last_results
|
188
|
+
stdout = cmd.displayer == :pipe ? StringIO.new : nil
|
189
|
+
value = eval_context.safe_eval(cmd.content, ctx, stdout)
|
190
|
+
else
|
191
|
+
value = cmd.content
|
192
|
+
end
|
193
|
+
|
194
|
+
return :done if value == 'exit' || value == 'quit'
|
195
|
+
|
196
|
+
if String === value
|
197
|
+
begin
|
198
|
+
last_results = MySQLResults.query(value, eval_context)
|
199
|
+
rescue MySQLResults::MaxRowsException => ex
|
200
|
+
$stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})"
|
201
|
+
rescue MysqlError => ex
|
202
|
+
$stderr.puts ex.message
|
203
|
+
rescue Exception => ex
|
204
|
+
$stderr.puts ex.inspect
|
205
|
+
raise
|
206
|
+
end
|
207
|
+
else
|
208
|
+
last_results = EvalResults.new(value, stdout)
|
209
|
+
end
|
210
|
+
|
211
|
+
break unless ctx.incomplete
|
212
|
+
end
|
213
|
+
|
214
|
+
return last_results
|
215
|
+
end
|
216
|
+
|
217
|
+
end # class Commands
|
218
|
+
|
219
|
+
end # module RSQL
|
@@ -0,0 +1,414 @@
|
|
1
|
+
# Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module RSQL
|
22
|
+
|
23
|
+
################################################################################
|
24
|
+
# This class wraps all dynamic evaluation and serves as the reflection
|
25
|
+
# class for adding new methods dynamically.
|
26
|
+
#
|
27
|
+
class EvalContext
|
28
|
+
|
29
|
+
Registration = Struct.new(:name, :args, :bangs, :block, :usage, :desc)
|
30
|
+
|
31
|
+
CommandContext = Struct.new(:index, :incomplete, :last_results, :state)
|
32
|
+
|
33
|
+
HEXSTR_LIMIT = 32
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@hexstr_limit = HEXSTR_LIMIT
|
37
|
+
@last_cmd = nil
|
38
|
+
|
39
|
+
@loaded_fns = []
|
40
|
+
@init_registrations = []
|
41
|
+
@bangs = {}
|
42
|
+
|
43
|
+
@registrations = {
|
44
|
+
:version => Registration.new('version', [], {},
|
45
|
+
method(:version),
|
46
|
+
'version',
|
47
|
+
'RSQL version information.'),
|
48
|
+
:reload => Registration.new('reload', [], {},
|
49
|
+
method(:reload),
|
50
|
+
'reload',
|
51
|
+
'Reload the rsqlrc file.'),
|
52
|
+
:last_cmd => Registration.new('last_cmd', [], {},
|
53
|
+
Proc.new{puts @last_cmd},
|
54
|
+
'last_cmd',
|
55
|
+
'Print the last command generated.'),
|
56
|
+
:set_max_rows => Registration.new('set_max_rows', [], {},
|
57
|
+
Proc.new{|r| MySQLResults.max_rows = r},
|
58
|
+
'set_max_rows',
|
59
|
+
'Set the maximum number of rows to process.'),
|
60
|
+
:max_rows => Registration.new('max_rows', [], {},
|
61
|
+
Proc.new{MySQLResults.max_rows},
|
62
|
+
'max_rows',
|
63
|
+
'Get the maximum number of rows to process.'),
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_accessor :bangs
|
68
|
+
|
69
|
+
def call_init_registrations(mysql)
|
70
|
+
@init_registrations.each do |sym|
|
71
|
+
reg = @registrations[sym]
|
72
|
+
sql = reg.block.call(*reg.args)
|
73
|
+
mysql.query(sql) if String === sql
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def load(fn)
|
78
|
+
ret = Thread.new {
|
79
|
+
begin
|
80
|
+
eval(File.read(fn), binding, fn)
|
81
|
+
nil
|
82
|
+
rescue Exception => ex
|
83
|
+
ex
|
84
|
+
end
|
85
|
+
}.value
|
86
|
+
|
87
|
+
if Exception === ret
|
88
|
+
bt = ret.backtrace.collect{|line| line.start_with?(fn) ? line : nil}.compact
|
89
|
+
$stderr.puts("#{ret.class}: #{ret.message}", bt, '')
|
90
|
+
else
|
91
|
+
@loaded_fns << fn unless @loaded_fns.include?(fn)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def reload
|
96
|
+
@loaded_fns.each{|fn| self.load(fn)}
|
97
|
+
puts "loaded: #{@loaded_fns.inspect}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def bang_eval(field, val)
|
101
|
+
if bang = @bangs[field]
|
102
|
+
begin
|
103
|
+
val = Thread.new{ eval("$SAFE=2;#{bang}(val)") }.value
|
104
|
+
rescue Exception => ex
|
105
|
+
$stderr.puts(ex.message, ex.backtrace.first)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return val
|
110
|
+
end
|
111
|
+
|
112
|
+
# safely evaluate Ruby content within our context
|
113
|
+
#
|
114
|
+
def safe_eval(content, context, stdout)
|
115
|
+
@command_context = context
|
116
|
+
|
117
|
+
# allow a simple reload to be called directly as it requires a
|
118
|
+
# little looser safety valve...
|
119
|
+
if 'reload' == content
|
120
|
+
reload
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
# same relaxed call to load too
|
125
|
+
if m = /^\s*load\s+'(.+)'\s*$/.match(content)
|
126
|
+
self.load(m[1])
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
if stdout
|
131
|
+
# capture stdout
|
132
|
+
orig_stdout = $stdout
|
133
|
+
$stdout = stdout
|
134
|
+
end
|
135
|
+
|
136
|
+
begin
|
137
|
+
value = Thread.new{ eval('$SAFE=2;' + content) }.value
|
138
|
+
rescue Exception => ex
|
139
|
+
$stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
|
140
|
+
ensure
|
141
|
+
$stdout = orig_stdout if stdout
|
142
|
+
end
|
143
|
+
|
144
|
+
@last_cmd = value if String === value
|
145
|
+
|
146
|
+
return value
|
147
|
+
end
|
148
|
+
|
149
|
+
# provide a list of tab completions given the prompted value
|
150
|
+
#
|
151
|
+
def complete(str)
|
152
|
+
if str[0] == ?.
|
153
|
+
str.slice!(0)
|
154
|
+
prefix = '.'
|
155
|
+
else
|
156
|
+
prefix = ''
|
157
|
+
end
|
158
|
+
|
159
|
+
ret = MySQLResults.complete(str)
|
160
|
+
ret += @registrations.keys.sort_by{|sym|sym.to_s}.collect do |sym|
|
161
|
+
name = sym.to_s
|
162
|
+
if name.start_with?(str)
|
163
|
+
prefix + name
|
164
|
+
else
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
ret.compact!
|
170
|
+
ret
|
171
|
+
end
|
172
|
+
|
173
|
+
# reset the hexstr limit back to the default value
|
174
|
+
#
|
175
|
+
def reset_hexstr_limit
|
176
|
+
@hexstr_limit = HEXSTR_LIMIT
|
177
|
+
end
|
178
|
+
|
179
|
+
# convert a binary string value into a hexadecimal string
|
180
|
+
#
|
181
|
+
def to_hexstr(bin, limit=@hexstr_limit, prefix='0x')
|
182
|
+
cnt = 0
|
183
|
+
str = prefix << bin.gsub(/./m) do |ch|
|
184
|
+
if limit
|
185
|
+
if limit < 1
|
186
|
+
cnt += 1
|
187
|
+
next
|
188
|
+
end
|
189
|
+
limit -= 1
|
190
|
+
end
|
191
|
+
'%02x' % ch[0]
|
192
|
+
end
|
193
|
+
|
194
|
+
if limit && limit < 1 && 0 < cnt
|
195
|
+
str << "... (#{cnt} bytes hidden)"
|
196
|
+
end
|
197
|
+
|
198
|
+
return str
|
199
|
+
end
|
200
|
+
|
201
|
+
########################################
|
202
|
+
private
|
203
|
+
|
204
|
+
# display a listing of all registered helpers
|
205
|
+
#
|
206
|
+
def list
|
207
|
+
usagelen = 0
|
208
|
+
desclen = 0
|
209
|
+
|
210
|
+
sorted = @registrations.values.sort_by do |reg|
|
211
|
+
usagelen = reg.usage.length if usagelen < reg.usage.length
|
212
|
+
longest_line = reg.desc.split(/\r?\n/).collect{|l|l.length}.max
|
213
|
+
desclen = longest_line if longest_line && desclen < longest_line
|
214
|
+
reg.usage
|
215
|
+
end
|
216
|
+
|
217
|
+
fmt = "%-#{usagelen}s %s#{$/}"
|
218
|
+
|
219
|
+
printf(fmt, 'usage', 'description')
|
220
|
+
puts '-'*(usagelen+2+desclen)
|
221
|
+
|
222
|
+
sorted.each do |reg|
|
223
|
+
printf(fmt, reg.usage, reg.desc)
|
224
|
+
end
|
225
|
+
|
226
|
+
return nil
|
227
|
+
end
|
228
|
+
|
229
|
+
# show all the pertinent version data we have about our
|
230
|
+
# software and the mysql connection
|
231
|
+
#
|
232
|
+
def version
|
233
|
+
puts "rsql:v#{RSQL::VERSION} client:v#{MySQLResults.conn.client_info} " \
|
234
|
+
"server:v#{MySQLResults.conn.server_info}"
|
235
|
+
end
|
236
|
+
|
237
|
+
# provide a helper utility in the event a registered
|
238
|
+
# method would like to make its own queries
|
239
|
+
#
|
240
|
+
def query(content, *args)
|
241
|
+
MySQLResults.query(content, self, *args)
|
242
|
+
end
|
243
|
+
|
244
|
+
# exactly like register below except in addition to registering as
|
245
|
+
# a usable call for later, we will also use these as soon as we
|
246
|
+
# have a connection to MySQL.
|
247
|
+
#
|
248
|
+
def register_init(sym, *args, &block)
|
249
|
+
register(sym, *args, &block)
|
250
|
+
@init_registrations << sym
|
251
|
+
end
|
252
|
+
|
253
|
+
# if given a block, allow the block to be called later, otherwise,
|
254
|
+
# create a method whose sole purpose is to dynmaically generate
|
255
|
+
# sql with variable interpolation
|
256
|
+
#
|
257
|
+
def register(sym, *args, &block)
|
258
|
+
name = usage = sym.to_s
|
259
|
+
|
260
|
+
if Hash === args.last
|
261
|
+
bangs = args.pop
|
262
|
+
desc = bangs.delete(:desc)
|
263
|
+
else
|
264
|
+
bangs = {}
|
265
|
+
end
|
266
|
+
|
267
|
+
desc = '' unless desc
|
268
|
+
|
269
|
+
if block.nil?
|
270
|
+
sql = sqeeze!(args.pop)
|
271
|
+
|
272
|
+
argstr = args.join(',')
|
273
|
+
usage << "(#{argstr})" unless argstr.empty?
|
274
|
+
|
275
|
+
blockstr = %{$SAFE=2;lambda{|#{argstr}|%{#{sql}} % [#{argstr}]}}
|
276
|
+
block = Thread.new{ eval(blockstr) }.value
|
277
|
+
args = []
|
278
|
+
else
|
279
|
+
usage << "(#{block.arity})" unless 0 == block.arity
|
280
|
+
end
|
281
|
+
|
282
|
+
@registrations[sym] = Registration.new(name, args, bangs, block, usage, desc)
|
283
|
+
end
|
284
|
+
|
285
|
+
# convert a collection of values into to hexadecimal strings
|
286
|
+
#
|
287
|
+
def hexify(*ids)
|
288
|
+
ids.collect do |id|
|
289
|
+
case id
|
290
|
+
when String
|
291
|
+
if id.start_with?('0x')
|
292
|
+
id
|
293
|
+
else
|
294
|
+
'0x' << id
|
295
|
+
end
|
296
|
+
when Integer
|
297
|
+
'0x' << id.to_s(16)
|
298
|
+
else
|
299
|
+
raise "invalid id: #{id.class}"
|
300
|
+
end
|
301
|
+
end.join(',')
|
302
|
+
end
|
303
|
+
|
304
|
+
# convert a number of bytes into a human readable string
|
305
|
+
#
|
306
|
+
def humanize_bytes(bytes)
|
307
|
+
abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
|
308
|
+
bytes = bytes.to_i
|
309
|
+
fmt = '%7.2f'
|
310
|
+
|
311
|
+
abbrev.each_with_index do |a,i|
|
312
|
+
if bytes < (1024**(i+1))
|
313
|
+
if i == 0
|
314
|
+
return "#{fmt % bytes} B"
|
315
|
+
else
|
316
|
+
b = bytes / (1024.0**i)
|
317
|
+
return "#{fmt % b} #{a}"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
return bytes.to_s
|
323
|
+
end
|
324
|
+
|
325
|
+
# convert a human readable string of bytes into an integer
|
326
|
+
#
|
327
|
+
def dehumanize_bytes(str)
|
328
|
+
abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
|
329
|
+
|
330
|
+
if str =~ /(\d+(\.\d+)?)\s*(\w+)?/
|
331
|
+
b = $1.to_f
|
332
|
+
if $3
|
333
|
+
i = abbrev.index($3.upcase)
|
334
|
+
return (b * (1024**i)).round
|
335
|
+
else
|
336
|
+
return b.round
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
raise "unable to parse '#{str}'"
|
341
|
+
end
|
342
|
+
|
343
|
+
# convert a time into a relative string from now
|
344
|
+
#
|
345
|
+
def relative_time(dt)
|
346
|
+
return dt unless String === dt
|
347
|
+
|
348
|
+
now = Time.now.utc
|
349
|
+
theirs = Time.parse(dt + ' UTC')
|
350
|
+
if theirs < now
|
351
|
+
diff = now - theirs
|
352
|
+
postfix = 'ago'
|
353
|
+
else
|
354
|
+
diff = theirs - now
|
355
|
+
postfix = 'from now'
|
356
|
+
end
|
357
|
+
|
358
|
+
fmt = '%3.0f'
|
359
|
+
|
360
|
+
[
|
361
|
+
[31556926.0, 'years'],
|
362
|
+
[2629743.83, 'months'],
|
363
|
+
[86400.0, 'days'],
|
364
|
+
[3600.0, 'hours'],
|
365
|
+
[60.0, 'minutes']
|
366
|
+
].each do |(limit, label)|
|
367
|
+
if (limit * 1.5) < diff
|
368
|
+
return "#{fmt % (diff / limit)} #{label} #{postfix}"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
return "#{fmt % diff} seconds #{postfix}"
|
373
|
+
end
|
374
|
+
|
375
|
+
# squeeze out any spaces
|
376
|
+
#
|
377
|
+
def sqeeze!(sql)
|
378
|
+
sql.gsub!(/\s+/,' ')
|
379
|
+
sql.strip!
|
380
|
+
sql << ';' unless sql[-1] == ?;
|
381
|
+
sql
|
382
|
+
end
|
383
|
+
|
384
|
+
# safely store an object into a file keeping at most one
|
385
|
+
# backup if the file already exists
|
386
|
+
#
|
387
|
+
def safe_save(obj, name)
|
388
|
+
name += '.yml' unless File.extname(name) == '.yml'
|
389
|
+
tn = "#{name}.tmp"
|
390
|
+
File.open(tn, 'w'){|f| YAML.dump(obj, f)}
|
391
|
+
if File.exist?(name)
|
392
|
+
bn = "#{name}~"
|
393
|
+
File.unlink(bn) if File.exist?(bn)
|
394
|
+
File.rename(name, bn)
|
395
|
+
end
|
396
|
+
File.rename(tn, name)
|
397
|
+
puts "Saved: #{name}"
|
398
|
+
end
|
399
|
+
|
400
|
+
def method_missing(sym, *args, &block)
|
401
|
+
if reg = @registrations[sym]
|
402
|
+
@bangs.merge!(reg.bangs)
|
403
|
+
final_args = reg.args + args
|
404
|
+
reg.block.call(*final_args)
|
405
|
+
elsif MySQLResults.respond_to?(sym)
|
406
|
+
MySQLResults.send(sym, *args)
|
407
|
+
else
|
408
|
+
super.method_missing(sym, *args, &block)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
end # class EvalContext
|
413
|
+
|
414
|
+
end # module RSQL
|