pprint.lua 14 KB

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