1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
90
91
92
93
94
95
96
97
98
99
100
101
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
192
193
194
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
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
|
--- Pretty-printing Lua tables.
-- Also provides a sandboxed Lua table reader and
-- a function to present large numbers in human-friendly format.
--
-- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
-- @module pl.pretty
local append = table.insert
local concat = table.concat
local mfloor, mhuge = math.floor, math.huge
local mtype = math.type
local utils = require 'pl.utils'
local lexer = require 'pl.lexer'
local debug = require 'debug'
local quote_string = require'pl.stringx'.quote_string
local assert_arg = utils.assert_arg
local original_tostring = tostring
-- Patch tostring to format numbers with better precision
-- and to produce cross-platform results for
-- infinite values and NaN.
local function tostring(value)
if type(value) ~= "number" then
return original_tostring(value)
elseif value ~= value then
return "NaN"
elseif value == mhuge then
return "Inf"
elseif value == -mhuge then
return "-Inf"
elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then
return ("%d"):format(value)
else
local res = ("%.14g"):format(value)
if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then
-- Number is internally a float but looks like an integer.
-- Insert ".0" after first run of digits.
res = res:gsub("%d+", "%0.0", 1)
end
return res
end
end
local pretty = {}
local function save_global_env()
local env = {}
env.hook, env.mask, env.count = debug.gethook()
-- env.hook is "external hook" if is a C hook function
if env.hook~="external hook" then
debug.sethook()
end
env.string_mt = getmetatable("")
debug.setmetatable("", nil)
return env
end
local function restore_global_env(env)
if env then
debug.setmetatable("", env.string_mt)
if env.hook~="external hook" then
debug.sethook(env.hook, env.mask, env.count)
end
end
end
--- Read a string representation of a Lua table.
-- This function loads and runs the string as Lua code, but bails out
-- if it contains a function definition.
-- Loaded string is executed in an empty environment.
-- @string s string to read in `{...}` format, possibly with some whitespace
-- before or after the curly braces. A single line comment may be present
-- at the beginning.
-- @return a table in case of success.
-- If loading the string failed, return `nil` and error message.
-- If executing loaded string failed, return `nil` and the error it raised.
function pretty.read(s)
assert_arg(1,s,'string')
if s:find '^%s*%-%-' then -- may start with a comment..
s = s:gsub('%-%-.-\n','')
end
if not s:find '^%s*{' then return nil,"not a Lua table" end
if s:find '[^\'"%w_]function[^\'"%w_]' then
local tok = lexer.lua(s)
for t,v in tok do
if t == 'keyword' and v == 'function' then
return nil,"cannot have functions in table definition"
end
end
end
s = 'return '..s
local chunk,err = utils.load(s,'tbl','t',{})
if not chunk then return nil,err end
local global_env = save_global_env()
local ok,ret = pcall(chunk)
restore_global_env(global_env)
if ok then return ret
else
return nil,ret
end
end
--- Read a Lua chunk.
-- @string s Lua code.
-- @tab[opt] env environment used to run the code, empty by default.
-- @bool[opt] paranoid abort loading if any looping constructs a found in the code
-- and disable string methods.
-- @return the environment in case of success or `nil` and syntax or runtime error
-- if something went wrong.
function pretty.load (s, env, paranoid)
env = env or {}
if paranoid then
local tok = lexer.lua(s)
for t,v in tok do
if t == 'keyword'
and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
then
return nil,"looping not allowed"
end
end
end
local chunk,err = utils.load(s,'tbl','t',env)
if not chunk then return nil,err end
local global_env = paranoid and save_global_env()
local ok,err = pcall(chunk)
restore_global_env(global_env)
if not ok then return nil,err end
return env
end
local function quote_if_necessary (v)
if not v then return ''
else
--AAS
if v:find ' ' then v = quote_string(v) end
end
return v
end
local keywords
local function is_identifier (s)
return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
end
local function quote (s)
if type(s) == 'table' then
return pretty.write(s,'')
else
--AAS
return quote_string(s)-- ('%q'):format(tostring(s))
end
end
local function index (numkey,key)
--AAS
if not numkey then
key = quote(key)
key = key:find("^%[") and (" " .. key .. " ") or key
end
return '['..key..']'
end
--- Create a string representation of a Lua table.
-- This function never fails, but may complain by returning an
-- extra value. Normally puts out one item per line, using
-- the provided indent; set the second parameter to an empty string
-- if you want output on one line.
--
-- *NOTE:* this is NOT a serialization function, not a full blown
-- debug function. Checkout out respectively the
-- [serpent](https://github.com/pkulchenko/serpent)
-- or [inspect](https://github.com/kikito/inspect.lua)
-- Lua modules for that if you need them.
-- @tab tbl Table to serialize to a string.
-- @string[opt] space The indent to use.
-- Defaults to two spaces; pass an empty string for no indentation.
-- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
-- Defaults to `false`.
-- @return a string
-- @return an optional error message
function pretty.write (tbl,space,not_clever)
if type(tbl) ~= 'table' then
local res = tostring(tbl)
if type(tbl) == 'string' then return quote(tbl) end
return res, 'not a table'
end
if not keywords then
keywords = lexer.get_keywords()
end
local set = ' = '
if space == '' then set = '=' end
space = space or ' '
local lines = {}
local line = ''
local tables = {}
local function put(s)
if #s > 0 then
line = line..s
end
end
local function putln (s)
if #line > 0 then
line = line..s
append(lines,line)
line = ''
else
append(lines,s)
end
end
local function eat_last_comma ()
local n = #lines
local lastch = lines[n]:sub(-1,-1)
if lastch == ',' then
lines[n] = lines[n]:sub(1,-2)
end
end
-- safe versions for iterators since 5.3+ honors metamethods that can throw
-- errors
local ipairs = function(t)
local i = 0
local ok, v
local getter = function() return t[i] end
return function()
i = i + 1
ok, v = pcall(getter)
if v == nil or not ok then return end
return i, t[i]
end
end
local pairs = function(t)
local k, v, ok
local getter = function() return next(t, k) end
return function()
ok, k, v = pcall(getter)
if not ok then return end
return k, v
end
end
local writeit
writeit = function (t,oldindent,indent)
local tp = type(t)
if tp ~= 'string' and tp ~= 'table' then
putln(quote_if_necessary(tostring(t))..',')
elseif tp == 'string' then
-- if t:find('\n') then
-- putln('[[\n'..t..']],')
-- else
-- putln(quote(t)..',')
-- end
--AAS
putln(quote_string(t) ..",")
elseif tp == 'table' then
if tables[t] then
putln('<cycle>,')
return
end
tables[t] = true
local newindent = indent..space
putln('{')
local used = {}
if not not_clever then
for i,val in ipairs(t) do
put(indent)
writeit(val,indent,newindent)
used[i] = true
end
end
local ordered_keys = {}
for k,v in pairs(t) do
if type(k) ~= 'number' then
ordered_keys[#ordered_keys + 1] = k
end
end
table.sort(ordered_keys, function (a, b)
if type(a) == type(b) and type(a) == 'string' then
return a < b
end
return type(a) == 'boolean' or (type(b) ~= 'boolean' and type(a) == 'table')
end)
local function write_entry (key, val)
local tkey = type(key)
local numkey = tkey == 'number'
if not_clever then
key = tostring(key)
put(indent..index(numkey,key)..set)
writeit(val,indent,newindent)
else
if not numkey or not used[key] then -- non-array indices
if tkey ~= 'string' then
key = tostring(key)
end
if numkey or not is_identifier(key) then
key = index(numkey,key)
end
put(indent..key..set)
writeit(val,indent,newindent)
end
end
end
for i = 1, #ordered_keys do
local key = ordered_keys[i]
local val = t[key]
write_entry(key, val)
end
for key,val in pairs(t) do
if type(key) == 'number' then
write_entry(key, val)
end
end
tables[t] = nil
eat_last_comma()
putln(oldindent..'},')
else
putln(tostring(t)..',')
end
end
writeit(tbl,'',space)
eat_last_comma()
return concat(lines,#space > 0 and '\n' or '')
end
--- Dump a Lua table out to a file or stdout.
-- @tab t The table to write to a file or stdout.
-- @string[opt] filename File name to write too. Defaults to writing
-- to stdout.
function pretty.dump (t, filename)
if not filename then
print(pretty.write(t))
return true
else
return utils.writefile(filename, pretty.write(t))
end
end
--- Dump a series of arguments to stdout for debug purposes.
-- This function is attached to the module table `__call` method, to make it
-- extra easy to access. So the full:
--
-- print(require("pl.pretty").write({...}))
--
-- Can be shortened to:
--
-- require"pl.pretty" (...)
--
-- Any `nil` entries will be printed as `"<nil>"` to make them explicit.
-- @param ... the parameters to dump to stdout.
-- @usage
-- -- example debug output
-- require"pl.pretty" ("hello", nil, "world", { bye = "world", true} )
--
-- -- output:
-- {
-- ["arg 1"] = "hello",
-- ["arg 2"] = "<nil>",
-- ["arg 3"] = "world",
-- ["arg 4"] = {
-- true,
-- bye = "world"
-- }
-- }
function pretty.debug(...)
local n = select("#", ...)
local t = { ... }
for i = 1, n do
local value = t[i]
if value == nil then
value = "<nil>"
end
t[i] = nil
t["arg " .. i] = value
end
print(pretty.write(t))
return true
end
local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
local function comma (val)
local thou = math.floor(val/1000)
if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
else return tostring(val) end
end
--- Format large numbers nicely for human consumption.
-- @number num a number.
-- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
-- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
-- or `'T'` (use commas as thousands separator), `'N'` by default.
-- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
function pretty.number (num,kind,prec)
local fmt = '%.'..(prec or 1)..'f%s'
if kind == 'T' then
return comma(num)
else
local postfixes, fact
if kind == 'M' then
fact = 1024
postfixes = memp
else
fact = 1000
postfixes = nump
end
local div = fact
local k = 1
while num >= div and k <= #postfixes do
div = div * fact
k = k + 1
end
div = div / fact
if k > #postfixes then k = k - 1; div = div/fact end
if k > 1 then
return fmt:format(num/div,postfixes[k] or 'duh')
else
return num..postfixes[1]
end
end
end
return setmetatable(pretty, {
__call = function(self, ...)
return self.debug(...)
end
})
|