Page 1 of 3

[library] inspect.lua - print tables and debug beautifully

Posted: Sun Apr 24, 2011 1:02 am
by kikito
Hi there!

I've created another utility function:

https://github.com/kikito/inspect.lua

It is mainly useful for debugging / logging errors; tables are displayed hierarchically. The main objective here is being easy to read for a human; It does not produce 100% lua-compatible code, so you should not use it for saving/restoring tables.

The way it handles "infinite recursion" (table t has a reference to table q, and table q references back to t) by simply limiting the "maximum depth" that will be used (this can be changed on a second value). EDIT: Nowadays it handles recursion much more awesomely.

I know that some of you have already developed your own "recursive printers". I hope this is useful for those who haven't.

The library does some fancy things with metatables. This was implemented to appease Taehl, who seems to be into that :P

You may find examples and instructions on the readme, which should be visible on the link above.

Regards!

Re: inspect.lua

Posted: Sun Apr 24, 2011 2:38 am
by TechnoCat
It is like PrettyPrinter for Lua. Thanks.

Re: inspect.lua

Posted: Sun Apr 24, 2011 11:33 am
by Lap
So much better than the basic functionality I've been using. Well done.

Re: inspect.lua

Posted: Sun Apr 24, 2011 11:39 am
by kikito
I'm glad you like it guys :)

Re: inspect.lua

Posted: Wed Apr 27, 2011 2:35 am
by ishkabible
i made a small improvement to it. with my version you can handle infinite recursion a bit differently. rather than only allowing for a limited depth my version allows you to pass a negative number to tell the 'putTable' method to keep going forever. this brings up the issue of infinite recursion, so it uses another table to keep a record of all parent tables and if the 'putTable' method is passed a table which has a parent of the same value then "<table: (hex value)" is printed instead.

example:

Code: Select all

inspect = require('inspect')

local t = {}
t.t = t
print("t = "..inspect(t,-1))
will print
t = {
t = <table: 00B7B928>
}
i like this better becuase its more flexible. i kinda want to add a way to show table that it recurs to but im not sure on how i want to do that yet.


entire code: (changes only made to Inspector:putTable and Inspector:new so that's all i posted)

Code: Select all

function Inspector:new(v, depth)
  local inspector = setmetatable( { buffer = {}, depth = depth, level = 0, RefTbl = {} }, {
    __index = Inspector,
    __tostring = function(instance) return table.concat(instance.buffer) end
  } )
  return inspector:putValue(v)
end

function Inspector:putTable(t)
	if self.RefTbl[t] then
		self:puts('<',tostring(t),'>')
	elseif self.depth > 0 and self.level >= self.depth then
		self:puts('{...}')
	else
		self.RefTbl[t] = true
		self:puts('{')
		self:down()

		local length = #t
		local mt = getmetatable(t)
		local __tostring = type(mt) == 'table' and mt.__tostring
		local string = type(__tostring) == 'function' and __tostring(t)

		if type(string) == 'string' and #string > 0 then
			self:puts(' -- ', unescape(string))
			if length >= 1 then self:tabify() end -- tabify the array values
		end

		local comma = false
		for i=1, length do
			comma = self:putComma(comma)
			self:puts(' '):putValue(t[i])
		end

		local dictKeys = getDictionaryKeys(t)

		for _,k in ipairs(dictKeys) do
			comma = self:putComma(comma)
			self:tabify():putKey(k):puts(' = '):putValue(t[k])
		end

		if mt then
			comma = self:putComma(comma)
			self:tabify():puts('<metatable> = '):putValue(mt)
		end
		self:up()

		if #dictKeys > 0 or mt then -- dictionary table. Justify closing }
			self:tabify()
		elseif length > 0 then -- array tables have one extra space before closing }
			self:puts(' ')
		end
		self:puts('}')
		self.RefTbl[t] = nil
	end
	return self
end

Re: inspect.lua

Posted: Wed Apr 27, 2011 8:34 am
by kikito
mm. I might steal your idea.

I'll do a rundown on my libs this weekend, there are some things I must change. Recursion handling on inspect might be one of the things that I change.

I like having a depth limit though. IMHO it increases readability.

Re: inspect.lua

Posted: Wed Apr 27, 2011 8:57 am
by BlackBulletIV
I like the idea of table IDs, that helps to distinguish one from another.

Re: inspect.lua

Posted: Wed Apr 27, 2011 11:34 am
by Robin
BlackBulletIV wrote:I like the idea of table IDs, that helps to distinguish one from another.
Perhaps something like

Code: Select all

t = {
t = <table: 00B7B928>
} -- 00B7B928
is useful, so the user can see which table the recursion refers to.

Re: inspect.lua

Posted: Wed Apr 27, 2011 10:43 pm
by ishkabible
I like having a depth limit though. IMHO it increases readability.
my alteration of your code keeps the depth as it is more readable, it just allows you to specify infinite depth as well.

ill try robins idea real quick to see how it looks with larger tables. do you know of anyway to get the hex value of the table with out just chopping of the first part of the tostring() output?

edit: i implemented robins idea, i looks pretty nice actually
added a function to get the hex string of a table

Code: Select all

local function tableHexStr(t)
  return string.sub(tostring(t), 7) 
end
and changed <following code> on line 147

Code: Select all

self:puts('}')
to

Code: Select all

self:puts('} -- ', tableHexStr(t))
edit2:
just encase anyone is confused on where I am (e.g. i refer to line 147) here is the entier code

Code: Select all

-----------------------------------------------------------------------------------------------------------------------
-- inspect.lua - v1.0 (2011-04)
-- Enrique García Cota - enrique.garcia.cota [AT] gmail [DOT] com
-- human-readable representations of tables.
-- inspired by http://lua-users.org/wiki/TableSerialization
-----------------------------------------------------------------------------------------------------------------------

-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
  if string.match( string.gsub(str,"[^'\"]",""), '^"+$' ) then
    return "'" .. str .. "'"
  end
  return string.format("%q", str )
end

local function tableHexStr(t)
  return string.sub(tostring(t), 7) 
end

local controlCharsTranslation = {
  ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
  ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\\"] = "\\\\"
}

local function unescapeChar(c) return controlCharsTranslation[c] end

local function unescape(str)
  local result, _ = string.gsub( str, "(%c)", unescapeChar )
  return result
end

local function isIdentifier(str)
  return string.match( str, "^[_%a][_%a%d]*$" )
end

local function isArrayKey(k, length)
  return type(k)=='number' and 1 <= k and k <= length
end

local function isDictionaryKey(k, length)
  return not isArrayKey(k, length)
end

local sortOrdersByType = {
  ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
  ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
}

function sortKeys(a,b)
  local ta, tb = type(a), type(b)
  if ta ~= tb then return sortOrdersByType[ta] < sortOrdersByType[tb] end
  if ta == 'string' or ta == 'number' then return a < b end
  return false
end

local function getDictionaryKeys(t)
  local length = #t
  local keys = {}
  for k,_ in pairs(t) do
    if isDictionaryKey(k, length) then table.insert(keys,k) end
  end
  table.sort(keys, sortKeys)
  return keys
end

local Inspector = {}

function Inspector:new(v, depth)
  local inspector = setmetatable( { buffer = {}, depth = depth, level = 0, RefTbl = {} }, {
    __index = Inspector,
    __tostring = function(instance) return table.concat(instance.buffer) end
  } )
  return inspector:putValue(v)
end

function Inspector:puts(...)
  local args = {...}
  for i=1, #args do
    table.insert(self.buffer, tostring(args[i]))
  end
  return self
end

function Inspector:tabify()
  self:puts("\n", string.rep(" ", self.level))
  return self
end

function Inspector:up()
  self.level = self.level - 1
end

function Inspector:down()
  self.level = self.level + 1
end

function Inspector:putComma(comma)
  if comma then self:puts(',') end
  return true
end

function Inspector:putTable(t)
	if self.RefTbl[t] then
		self:puts('<',tostring(t),'>')
	elseif self.depth > 0 and self.level >= self.depth then
		self:puts('{...}')
	else
		self.RefTbl[t] = true
		self:puts('{')
		self:down()

		local length = #t
		local mt = getmetatable(t)
		local __tostring = type(mt) == 'table' and mt.__tostring
		local string = type(__tostring) == 'function' and __tostring(t)

		if type(string) == 'string' and #string > 0 then
			self:puts(' -- ', unescape(string))
			if length >= 1 then self:tabify() end -- tabify the array values
		end

		local comma = false
		for i=1, length do
			comma = self:putComma(comma)
			self:puts(' '):putValue(t[i])
		end

		local dictKeys = getDictionaryKeys(t)

		for _,k in ipairs(dictKeys) do
			comma = self:putComma(comma)
			self:tabify():putKey(k):puts(' = '):putValue(t[k])
		end

		if mt then
			comma = self:putComma(comma)
			self:tabify():puts('<metatable> = '):putValue(mt)
		end
		self:up()

		if #dictKeys > 0 or mt then -- dictionary table. Justify closing }
			self:tabify()
		elseif length > 0 then -- array tables have one extra space before closing }
			self:puts(' ')
		end
		self:puts('} -- ', tableHexStr(t))
		self.RefTbl[t] = nil
	end
	return self
end

function Inspector:putValue(v)
  local tv = type(v)

  if tv == 'string' then
    self:puts(smartQuote(unescape(v)))
  elseif tv == 'number' or tv == 'boolean' then
    self:puts(tostring(v))
  elseif tv == 'table' then
    self:putTable(v)
  else
    self:puts('<',tv,'>')
  end
  return self
end

function Inspector:putKey(k)
  if type(k) == "string" and isIdentifier(k) then
    return self:puts(k)
  end
  return self:puts( "[" ):putValue(k):puts("]")
end

local function inspect(t, depth)
  depth = depth or 4
  return tostring(Inspector:new(t, depth))
end

return inspect

Re: inspect.lua

Posted: Thu Apr 28, 2011 4:37 am
by Robin

Code: Select all

if string.match( string.gsub(str,"[^'\"]",""), '^"+$' ) then
Wouldn't it make more sense to use this:

Code: Select all

if str:find('"', 1, true) and not str:find("'", 1, true) then