1local pprint = { VERSION = '0.1' } 2 3pprint.defaults = { 4 -- type display trigger, hide not useful datatypes by default 5 -- custom types are treated as table 6 show_nil = true, 7 show_boolean = true, 8 show_number = true, 9 show_string = true, 10 show_table = true, 11 show_function = false, 12 show_thread = false, 13 show_userdata = false, 14 -- additional display trigger 15 show_metatable = false, -- show metatable 16 show_all = false, -- override other show settings and show everything 17 use_tostring = false, -- use __tostring to print table if available 18 filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide 19 object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache 20 -- per process, falsy value to disable (might cause infinite loop) 21 -- format settings 22 indent_size = 2, -- indent for each nested table level 23 level_width = 80, -- max width per indent level 24 wrap_string = true, -- wrap string when it's longer than level_width 25 wrap_array = false, -- wrap every array elements 26 sort_keys = true, -- sort table keys 27} 28 29local TYPES = { 30 ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4, 31 ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8 32} 33 34-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a' 35local ESCAPE_MAP = { 36 ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', 37 ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\', 38} 39 40-- generic utilities 41local function escape(s) 42 s = s:gsub('([%c\\])', ESCAPE_MAP) 43 local dq = s:find('"') 44 local sq = s:find("'") 45 if dq and sq then 46 return s:gsub('"', '\\"'), '"' 47 elseif sq then 48 return s, '"' 49 else 50 return s, "'" 51 end 52end 53 54local function is_plain_key(key) 55 return type(key) == 'string' and key:match('^[%a_][%a%d_]*$') 56end 57 58local CACHE_TYPES = { 59 ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true 60} 61 62-- cache would be populated to be like: 63-- { 64-- function = { `fun1` = 1, _cnt = 1 }, -- object id 65-- table = { `table1` = 1, `table2` = 2, _cnt = 2 }, 66-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count 67-- } 68-- use weakrefs to avoid accidentall adding refcount 69local function cache_apperance(obj, cache, option) 70 if not cache.visited_tables then 71 cache.visited_tables = setmetatable({}, {__mode = 'k'}) 72 end 73 local t = type(obj) 74 75 -- TODO can't test filter_function here as we don't have the ix and key, 76 -- might cause different results? 77 -- respect show_xxx and filter_function to be consistent with print results 78 if (not TYPES[t] and not option.show_table) 79 or (TYPES[t] and not option['show_'..t]) then 80 return 81 end 82 83 if CACHE_TYPES[t] or TYPES[t] == nil then 84 if not cache[t] then 85 cache[t] = setmetatable({}, {__mode = 'k'}) 86 cache[t]._cnt = 0 87 end 88 if not cache[t][obj] then 89 cache[t]._cnt = cache[t]._cnt + 1 90 cache[t][obj] = cache[t]._cnt 91 end 92 end 93 if t == 'table' or TYPES[t] == nil then 94 if cache.visited_tables[obj] == false then 95 -- already printed, no need to mark this and its children anymore 96 return 97 elseif cache.visited_tables[obj] == nil then 98 cache.visited_tables[obj] = 1 99 else 100 -- visited already, increment and continue 101 cache.visited_tables[obj] = cache.visited_tables[obj] + 1 102 return 103 end 104 for k, v in pairs(obj) do 105 cache_apperance(k, cache, option) 106 cache_apperance(v, cache, option) 107 end 108 local mt = getmetatable(obj) 109 if mt and option.show_metatable then 110 cache_apperance(mt, cache, option) 111 end 112 end 113end 114 115-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method 116local function str_natural_cmp(lhs, rhs) 117 while #lhs > 0 and #rhs > 0 do 118 local lmid, lend = lhs:find('%d+') 119 local rmid, rend = rhs:find('%d+') 120 if not (lmid and rmid) then return lhs < rhs end 121 122 local lsub = lhs:sub(1, lmid-1) 123 local rsub = rhs:sub(1, rmid-1) 124 if lsub ~= rsub then 125 return lsub < rsub 126 end 127 128 local lnum = tonumber(lhs:sub(lmid, lend)) 129 local rnum = tonumber(rhs:sub(rmid, rend)) 130 if lnum ~= rnum then 131 return lnum < rnum 132 end 133 134 lhs = lhs:sub(lend+1) 135 rhs = rhs:sub(rend+1) 136 end 137 return lhs < rhs 138end 139 140local function cmp(lhs, rhs) 141 local tleft = type(lhs) 142 local tright = type(rhs) 143 if tleft == 'number' and tright == 'number' then return lhs < rhs end 144 if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end 145 if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end 146 147 -- allow custom types 148 local oleft = TYPES[tleft] or 9 149 local oright = TYPES[tright] or 9 150 return oleft < oright 151end 152 153-- setup option with default 154local function make_option(option) 155 if option == nil then 156 option = {} 157 end 158 for k, v in pairs(pprint.defaults) do 159 if option[k] == nil then 160 option[k] = v 161 end 162 if option.show_all then 163 for t, _ in pairs(TYPES) do 164 option['show_'..t] = true 165 end 166 option.show_metatable = true 167 end 168 end 169 return option 170end 171 172-- override defaults and take effects for all following calls 173function pprint.setup(option) 174 pprint.defaults = make_option(option) 175end 176 177-- format lua object into a string 178function pprint.pformat(obj, option, printer) 179 option = make_option(option) 180 local buf = {} 181 local function default_printer(s) 182 table.insert(buf, s) 183 end 184 printer = printer or default_printer 185 186 local cache 187 if option.object_cache == 'global' then 188 -- steal the cache into a local var so it's not visible from _G or anywhere 189 -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway 190 cache = pprint._cache or {} 191 pprint._cache = nil 192 elseif option.object_cache == 'local' then 193 cache = {} 194 end 195 196 local last = '' -- used for look back and remove trailing comma 197 local status = { 198 indent = '', -- current indent 199 len = 0, -- current line length 200 } 201 202 local wrapped_printer = function(s) 203 printer(last) 204 last = s 205 end 206 207 local function _indent(d) 208 status.indent = string.rep(' ', d + #(status.indent)) 209 end 210 211 local function _n(d) 212 wrapped_printer('\n') 213 wrapped_printer(status.indent) 214 if d then 215 _indent(d) 216 end 217 status.len = 0 218 return true -- used to close bracket correctly 219 end 220 221 local function _p(s, nowrap) 222 status.len = status.len + #s 223 if not nowrap and status.len > option.level_width then 224 _n() 225 wrapped_printer(s) 226 status.len = #s 227 else 228 wrapped_printer(s) 229 end 230 end 231 232 local formatter = {} 233 local function format(v) 234 local f = formatter[type(v)] 235 f = f or formatter.table -- allow patched type() 236 if option.filter_function and option.filter_function(v, nil, nil) then 237 return '' 238 else 239 return f(v) 240 end 241 end 242 243 local function tostring_formatter(v) 244 return tostring(v) 245 end 246 247 local function number_formatter(n) 248 return n == math.huge and '[[math.huge]]' or tostring(n) 249 end 250 251 local function nop_formatter(v) 252 return '' 253 end 254 255 local function make_fixed_formatter(t, has_cache) 256 if has_cache then 257 return function (v) 258 return string.format('[[%s %d]]', t, cache[t][v]) 259 end 260 else 261 return function (v) 262 return '[['..t..']]' 263 end 264 end 265 end 266 267 local function string_formatter(s, force_long_quote) 268 local s, quote = escape(s) 269 local quote_len = force_long_quote and 4 or 2 270 if quote_len + #s + status.len > option.level_width then 271 _n() 272 -- only wrap string when is longer than level_width 273 if option.wrap_string and #s + quote_len > option.level_width then 274 -- keep the quotes together 275 _p('[[') 276 while #s + status.len >= option.level_width do 277 local seg = option.level_width - status.len 278 _p(string.sub(s, 1, seg), true) 279 _n() 280 s = string.sub(s, seg+1) 281 end 282 _p(s) -- print the remaining parts 283 return ']]' 284 end 285 end 286 287 return force_long_quote and '[['..s..']]' or quote..s..quote 288 end 289 290 local function table_formatter(t) 291 if option.use_tostring then 292 local mt = getmetatable(t) 293 if mt and mt.__tostring then 294 return string_formatter(tostring(t), true) 295 end 296 end 297 298 local print_header_ix = nil 299 local ttype = type(t) 300 if option.object_cache then 301 local cache_state = cache.visited_tables[t] 302 local tix = cache[ttype][t] 303 -- FIXME should really handle `cache_state == nil` 304 -- as user might add things through filter_function 305 if cache_state == false then 306 -- already printed, just print the the number 307 return string_formatter(string.format('%s %d', ttype, tix), true) 308 elseif cache_state > 1 then 309 -- appeared more than once, print table header with number 310 print_header_ix = tix 311 cache.visited_tables[t] = false 312 else 313 -- appeared exactly once, print like a normal table 314 end 315 end 316 317 local tlen = #t 318 local wrapped = false 319 _p('{') 320 _indent(option.indent_size) 321 _p(string.rep(' ', option.indent_size - 1)) 322 if print_header_ix then 323 _p(string.format('--[[%s %d]] ', ttype, print_header_ix)) 324 end 325 for ix = 1,tlen do 326 local v = t[ix] 327 if formatter[type(v)] == nop_formatter or 328 (option.filter_function and option.filter_function(v, ix, t)) then 329 -- pass 330 else 331 if option.wrap_array then 332 wrapped = _n() 333 end 334 _p(format(v)..', ') 335 end 336 end 337 338 -- hashmap part of the table, in contrast to array part 339 local function is_hash_key(k) 340 local numkey = tonumber(k) 341 if numkey ~= k or numkey > tlen then 342 return true 343 end 344 end 345 346 local function print_kv(k, v, t) 347 -- can't use option.show_x as obj may contain custom type 348 if formatter[type(v)] == nop_formatter or 349 formatter[type(k)] == nop_formatter or 350 (option.filter_function and option.filter_function(v, k, t)) then 351 return 352 end 353 wrapped = _n() 354 if is_plain_key(k) then 355 _p(k, true) 356 else 357 _p('[') 358 -- [[]] type string in key is illegal, needs to add spaces inbetween 359 local k = format(k) 360 if string.match(k, '%[%[') then 361 _p(' '..k..' ', true) 362 else 363 _p(k, true) 364 end 365 _p(']') 366 end 367 _p(' = ', true) 368 _p(format(v), true) 369 _p(',', true) 370 end 371 372 if option.sort_keys then 373 local keys = {} 374 for k, _ in pairs(t) do 375 if is_hash_key(k) then 376 table.insert(keys, k) 377 end 378 end 379 table.sort(keys, cmp) 380 for _, k in ipairs(keys) do 381 print_kv(k, t[k], t) 382 end 383 else 384 for k, v in pairs(t) do 385 if is_hash_key(k) then 386 print_kv(k, v, t) 387 end 388 end 389 end 390 391 if option.show_metatable then 392 local mt = getmetatable(t) 393 if mt then 394 print_kv('__metatable', mt, t) 395 end 396 end 397 398 _indent(-option.indent_size) 399 -- make { } into {} 400 last = string.gsub(last, '^ +$', '') 401 -- peek last to remove trailing comma 402 last = string.gsub(last, ',%s*$', ' ') 403 if wrapped then 404 _n() 405 end 406 _p('}') 407 408 return '' 409 end 410 411 -- set formatters 412 formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter 413 formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter 414 formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge 415 formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter 416 formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter 417 formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter 418 formatter['string'] = option.show_string and string_formatter or nop_formatter 419 formatter['table'] = option.show_table and table_formatter or nop_formatter 420 421 if option.object_cache then 422 -- needs to visit the table before start printing 423 cache_apperance(obj, cache, option) 424 end 425 426 _p(format(obj)) 427 printer(last) -- close the buffered one 428 429 -- put cache back if global 430 if option.object_cache == 'global' then 431 pprint._cache = cache 432 end 433 434 return table.concat(buf) 435end 436 437-- pprint all the arguments 438function pprint.pprint( ... ) 439 local args = {...} 440 -- select will get an accurate count of array len, counting trailing nils 441 local len = select('#', ...) 442 for ix = 1,len do 443 pprint.pformat(args[ix], nil, io.write) 444 io.write('\n') 445 end 446end 447 448setmetatable(pprint, { 449 __call = function (_, ...) 450 pprint.pprint(...) 451 end 452}) 453 454return pprint 455 456