• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 4655740294

pending completion
4655740294

push

github

GitHub
fix(template): using magic characters as escapes (#452)

2 of 2 new or added lines in 1 file covered. (100.0%)

5279 of 6022 relevant lines covered (87.66%)

40.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.13
/lua/pl/pretty.lua
1
--- Pretty-printing Lua tables.
2
-- Also provides a sandboxed Lua table reader and
3
-- a function to present large numbers in human-friendly format.
4
--
5
-- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
6
-- @module pl.pretty
7

8
local append = table.insert
35✔
9
local concat = table.concat
35✔
10
local mfloor, mhuge = math.floor, math.huge
35✔
11
local mtype = math.type
35✔
12
local utils = require 'pl.utils'
35✔
13
local lexer = require 'pl.lexer'
35✔
14
local debug = require 'debug'
35✔
15
local quote_string = require'pl.stringx'.quote_string
35✔
16
local assert_arg = utils.assert_arg
35✔
17

18
local original_tostring = tostring
35✔
19

20
-- Patch tostring to format numbers with better precision
21
-- and to produce cross-platform results for
22
-- infinite values and NaN.
23
local function tostring(value)
24
    if type(value) ~= "number" then
82✔
25
        return original_tostring(value)
44✔
26
    elseif value ~= value then
38✔
27
        return "NaN"
1✔
28
    elseif value == mhuge then
37✔
29
        return "Inf"
1✔
30
    elseif value == -mhuge then
36✔
31
        return "-Inf"
1✔
32
    elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then
35✔
33
        return ("%d"):format(value)
34✔
34
    else
35
        local res = ("%.14g"):format(value)
1✔
36
        if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then
1✔
37
            -- Number is internally a float but looks like an integer.
38
            -- Insert ".0" after first run of digits.
39
            res = res:gsub("%d+", "%0.0", 1)
×
40
        end
41
        return res
1✔
42
    end
43
end
44

45
local pretty = {}
35✔
46

47
local function save_global_env()
48
    local env = {}
4✔
49
    env.hook, env.mask, env.count = debug.gethook()
4✔
50

51
    -- env.hook is "external hook" if is a C hook function
52
    if env.hook~="external hook" then
4✔
53
        debug.sethook()
4✔
54
    end
55

56
    env.string_mt = getmetatable("")
×
57
    debug.setmetatable("", nil)
×
58
    return env
×
59
end
60

61
local function restore_global_env(env)
62
    if env then
1✔
63
        debug.setmetatable("", env.string_mt)
×
64
        if env.hook~="external hook" then
×
65
            debug.sethook(env.hook, env.mask, env.count)
×
66
        end
67
    end
68
end
69

70
--- Read a string representation of a Lua table.
71
-- This function loads and runs the string as Lua code, but bails out
72
-- if it contains a function definition.
73
-- Loaded string is executed in an empty environment.
74
-- @string s string to read in `{...}` format, possibly with some whitespace
75
-- before or after the curly braces. A single line comment may be present
76
-- at the beginning.
77
-- @return a table in case of success.
78
-- If loading the string failed, return `nil` and error message.
79
-- If executing loaded string failed, return `nil` and the error it raised.
80
function pretty.read(s)
35✔
81
    assert_arg(1,s,'string')
5✔
82
    if s:find '^%s*%-%-' then -- may start with a comment..
5✔
83
        s = s:gsub('%-%-.-\n','')
×
84
    end
85
    if not s:find '^%s*{' then return nil,"not a Lua table" end
5✔
86
    if s:find '[^\'"%w_]function[^\'"%w_]' then
5✔
87
        local tok = lexer.lua(s)
2✔
88
        for t,v in tok do
27✔
89
            if t == 'keyword' and v == 'function' then
26✔
90
                return nil,"cannot have functions in table definition"
1✔
91
            end
92
        end
93
    end
94
    s = 'return '..s
4✔
95
    local chunk,err = utils.load(s,'tbl','t',{})
4✔
96
    if not chunk then return nil,err end
4✔
97
    local global_env = save_global_env()
4✔
98
    local ok,ret = pcall(chunk)
×
99
    restore_global_env(global_env)
×
100
    if ok then return ret
4✔
101
    else
102
        return nil,ret
1✔
103
    end
104
end
105

106
--- Read a Lua chunk.
107
-- @string s Lua code.
108
-- @tab[opt] env environment used to run the code, empty by default.
109
-- @bool[opt] paranoid abort loading if any looping constructs a found in the code
110
-- and disable string methods.
111
-- @return the environment in case of success or `nil` and syntax or runtime error
112
-- if something went wrong.
113
function pretty.load (s, env, paranoid)
35✔
114
    env = env or {}
2✔
115
    if paranoid then
2✔
116
        local tok = lexer.lua(s)
1✔
117
        for t,v in tok do
4✔
118
            if t == 'keyword'
4✔
119
                and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
1✔
120
            then
121
                return nil,"looping not allowed"
1✔
122
            end
123
        end
124
    end
125
    local chunk,err = utils.load(s,'tbl','t',env)
1✔
126
    if not chunk then return nil,err end
1✔
127
    local global_env = paranoid and save_global_env()
1✔
128
    local ok,err = pcall(chunk)
1✔
129
    restore_global_env(global_env)
1✔
130
    if not ok then return nil,err end
1✔
131
    return env
1✔
132
end
133

134
local function quote_if_necessary (v)
135
    if not v then return ''
28✔
136
    else
137
        --AAS
138
        if v:find ' ' then v = quote_string(v) end
28✔
139
    end
140
    return v
28✔
141
end
142

143
local keywords
144

145
local function is_identifier (s)
146
    return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
14✔
147
end
148

149
local function quote (s)
150
    if type(s) == 'table' then
4✔
151
        return pretty.write(s,'')
×
152
    else
153
        --AAS
154
        return quote_string(s)-- ('%q'):format(tostring(s))
4✔
155
    end
156
end
157

158
local function index (numkey,key)
159
    --AAS
160
    if not numkey then
6✔
161
        key = quote(key)
4✔
162
         key = key:find("^%[") and (" " .. key .. " ") or key
4✔
163
    end
164
    return '['..key..']'
6✔
165
end
166

167

168
--- Create a string representation of a Lua table.
169
-- This function never fails, but may complain by returning an
170
-- extra value. Normally puts out one item per line, using
171
-- the provided indent; set the second parameter to an empty string
172
-- if you want output on one line.
173
--
174
-- *NOTE:* this is NOT a serialization function, not a full blown
175
-- debug function. Checkout out respectively the
176
-- [serpent](https://github.com/pkulchenko/serpent)
177
-- or [inspect](https://github.com/kikito/inspect.lua)
178
-- Lua modules for that if you need them.
179
-- @tab tbl Table to serialize to a string.
180
-- @string[opt] space The indent to use.
181
-- Defaults to two spaces; pass an empty string for no indentation.
182
-- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
183
-- Defaults to `false`.
184
-- @return a string
185
-- @return an optional error message
186
function pretty.write (tbl,space,not_clever)
35✔
187
    if type(tbl) ~= 'table' then
14✔
188
        local res = tostring(tbl)
×
189
        if type(tbl) == 'string' then return quote(tbl) end
×
190
        return res, 'not a table'
×
191
    end
192
    if not keywords then
14✔
193
        keywords = lexer.get_keywords()
3✔
194
    end
195
    local set = ' = '
14✔
196
    if space == '' then set = '=' end
14✔
197
    space = space or '  '
14✔
198
    local lines = {}
14✔
199
    local line = ''
14✔
200
    local tables = {}
14✔
201

202

203
    local function put(s)
204
        if #s > 0 then
49✔
205
            line = line..s
25✔
206
        end
207
    end
208

209
    local function putln (s)
210
        if #line > 0 then
82✔
211
            line = line..s
25✔
212
            append(lines,line)
25✔
213
            line = ''
25✔
214
        else
215
            append(lines,s)
57✔
216
        end
217
    end
218

219
    local function eat_last_comma ()
220
        local n = #lines
33✔
221
        local lastch = lines[n]:sub(-1,-1)
33✔
222
        if lastch == ',' then
33✔
223
            lines[n] = lines[n]:sub(1,-2)
29✔
224
        end
225
    end
226

227

228
    -- safe versions for iterators since 5.3+ honors metamethods that can throw
229
    -- errors
230
    local ipairs = function(t)
231
        local i = 0
19✔
232
        local ok, v
233
        local getter = function() return t[i] end
71✔
234
        return function()
235
                i = i + 1
52✔
236
                ok, v = pcall(getter)
52✔
237
                if v == nil or not ok then return end
52✔
238
                return i, t[i]
33✔
239
            end
240
    end
241
    local pairs = function(t)
242
        local k, v, ok
243
        local getter = function() return next(t, k) end
174✔
244
        return function()
245
                ok, k, v = pcall(getter)
136✔
246
                if not ok then return end
136✔
247
                return k, v
136✔
248
            end
249
    end
250

251
    local writeit
252
    writeit = function (t,oldindent,indent)
253
        local tp = type(t)
63✔
254
        if tp ~= 'string' and  tp ~= 'table' then
63✔
255
            putln(quote_if_necessary(tostring(t))..',')
28✔
256
        elseif tp == 'string' then
35✔
257
            -- if t:find('\n') then
258
            --     putln('[[\n'..t..']],')
259
            -- else
260
            --     putln(quote(t)..',')
261
            -- end
262
            --AAS
263
            putln(quote_string(t) ..",")
14✔
264
        elseif tp == 'table' then
21✔
265
            if tables[t] then
21✔
266
                putln('<cycle>,')
2✔
267
                return
2✔
268
            end
269
            tables[t] = true
19✔
270
            local newindent = indent..space
19✔
271
            putln('{')
19✔
272
            local used = {}
19✔
273
            if not not_clever then
19✔
274
                for i,val in ipairs(t) do
52✔
275
                    put(indent)
33✔
276
                    writeit(val,indent,newindent)
33✔
277
                    used[i] = true
33✔
278
                end
279
            end
280
            local ordered_keys = {}
19✔
281
            for k,v in pairs(t) do
68✔
282
               if type(k) ~= 'number' then
49✔
283
                  ordered_keys[#ordered_keys + 1] = k
14✔
284
               end
285
            end
286
            table.sort(ordered_keys, function (a, b)
38✔
287
                if type(a) == type(b) then
22✔
288
                    return tostring(a) < tostring(b)
18✔
289
                else
290
                    return type(a) < type(b)
4✔
291
                end
292
            end)
293
            local function write_entry (key, val)
294
                local tkey = type(key)
49✔
295
                local numkey = tkey == 'number'
49✔
296
                if not_clever then
49✔
297
                    key = tostring(key)
×
298
                    put(indent..index(numkey,key)..set)
×
299
                    writeit(val,indent,newindent)
×
300
                else
301
                    if not numkey or not used[key] then -- non-array indices
49✔
302
                        if tkey ~= 'string' then
16✔
303
                            key = tostring(key)
5✔
304
                        end
305
                        if numkey or not is_identifier(key) then
16✔
306
                            key = index(numkey,key)
6✔
307
                        end
308
                        put(indent..key..set)
16✔
309
                        writeit(val,indent,newindent)
16✔
310
                    end
311
                end
312
            end
313
            for i = 1, #ordered_keys do
33✔
314
                local key = ordered_keys[i]
14✔
315
                local val = t[key]
14✔
316
                write_entry(key, val)
14✔
317
            end
318
            for key,val in pairs(t) do
68✔
319
               if type(key) == 'number' then
49✔
320
                  write_entry(key, val)
35✔
321
               end
322
            end
323
            tables[t] = nil
19✔
324
            eat_last_comma()
19✔
325
            putln(oldindent..'},')
19✔
326
        else
327
            putln(tostring(t)..',')
×
328
        end
329
    end
330
    writeit(tbl,'',space)
14✔
331
    eat_last_comma()
14✔
332
    return concat(lines,#space > 0 and '\n' or '')
14✔
333
end
334

335
--- Dump a Lua table out to a file or stdout.
336
-- @tab t The table to write to a file or stdout.
337
-- @string[opt] filename File name to write too. Defaults to writing
338
-- to stdout.
339
function pretty.dump (t, filename)
35✔
340
    if not filename then
×
341
        print(pretty.write(t))
×
342
        return true
×
343
    else
344
        return utils.writefile(filename, pretty.write(t))
×
345
    end
346
end
347

348
--- Dump a series of arguments to stdout for debug purposes.
349
-- This function is attached to the module table `__call` method, to make it
350
-- extra easy to access. So the full:
351
--
352
--     print(require("pl.pretty").write({...}))
353
--
354
-- Can be shortened to:
355
--
356
--     require"pl.pretty" (...)
357
--
358
-- Any `nil` entries will be printed as `"<nil>"` to make them explicit.
359
-- @param ... the parameters to dump to stdout.
360
-- @usage
361
-- -- example debug output
362
-- require"pl.pretty" ("hello", nil, "world", { bye = "world", true} )
363
--
364
-- -- output:
365
-- {
366
--   ["arg 1"] = "hello",
367
--   ["arg 2"] = "<nil>",
368
--   ["arg 3"] = "world",
369
--   ["arg 4"] = {
370
--     true,
371
--     bye = "world"
372
--   }
373
-- }
374
function pretty.debug(...)
35✔
375
    local n = select("#", ...)
×
376
    local t = { ... }
×
377
    for i = 1, n do
×
378
        local value = t[i]
×
379
        if value == nil then
×
380
            value = "<nil>"
×
381
        end
382
        t[i] = nil
×
383
        t["arg " .. i] = value
×
384
    end
385

386
    print(pretty.write(t))
×
387
    return true
×
388
end
389

390

391
local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
35✔
392

393
local function comma (val)
394
    local thou = math.floor(val/1000)
13✔
395
    if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
13✔
396
    else return tostring(val) end
6✔
397
end
398

399
--- Format large numbers nicely for human consumption.
400
-- @number num a number.
401
-- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
402
-- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
403
-- or `'T'` (use commas as thousands separator), `'N'` by default.
404
-- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
405
function pretty.number (num,kind,prec)
35✔
406
    local fmt = '%.'..(prec or 1)..'f%s'
16✔
407
    if kind == 'T' then
16✔
408
        return comma(num)
6✔
409
    else
410
        local postfixes, fact
411
        if kind == 'M' then
10✔
412
            fact = 1024
5✔
413
            postfixes = memp
5✔
414
        else
415
            fact = 1000
5✔
416
            postfixes = nump
5✔
417
        end
418
        local div = fact
10✔
419
        local k = 1
10✔
420
        while num >= div and k <= #postfixes do
24✔
421
            div = div * fact
14✔
422
            k = k + 1
14✔
423
        end
424
        div = div / fact
10✔
425
        if k > #postfixes then k = k - 1; div = div/fact end
10✔
426
        if k > 1 then
10✔
427
            return fmt:format(num/div,postfixes[k] or 'duh')
8✔
428
        else
429
            return num..postfixes[1]
2✔
430
        end
431
    end
432
end
433

434
return setmetatable(pretty, {
70✔
435
    __call = function(self, ...)
436
        return self.debug(...)
×
437
    end
438
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc