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

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
kikito
Inner party member
Posts: 3151
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

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

Post by kikito » Sun Apr 24, 2011 1:02 am

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!
Last edited by kikito on Mon Oct 29, 2012 11:23 pm, edited 3 times in total.
When I write def I mean function.

User avatar
TechnoCat
Inner party member
Posts: 1611
Joined: Thu Jul 30, 2009 12:31 am
Location: Denver, CO
Contact:

Re: inspect.lua

Post by TechnoCat » Sun Apr 24, 2011 2:38 am

It is like PrettyPrinter for Lua. Thanks.

User avatar
Lap
Party member
Posts: 256
Joined: Fri Apr 30, 2010 3:46 pm

Re: inspect.lua

Post by Lap » Sun Apr 24, 2011 11:33 am

So much better than the basic functionality I've been using. Well done.

User avatar
kikito
Inner party member
Posts: 3151
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: inspect.lua

Post by kikito » Sun Apr 24, 2011 11:39 am

I'm glad you like it guys :)
When I write def I mean function.

User avatar
ishkabible
Party member
Posts: 241
Joined: Sat Oct 23, 2010 7:34 pm
Location: Kansas USA

Re: inspect.lua

Post by ishkabible » Wed Apr 27, 2011 2:35 am

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

User avatar
kikito
Inner party member
Posts: 3151
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: inspect.lua

Post by kikito » Wed Apr 27, 2011 8:34 am

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.
When I write def I mean function.

User avatar
BlackBulletIV
Inner party member
Posts: 1260
Joined: Wed Dec 29, 2010 8:19 pm
Location: Queensland, Australia
Contact:

Re: inspect.lua

Post by BlackBulletIV » Wed Apr 27, 2011 8:57 am

I like the idea of table IDs, that helps to distinguish one from another.

User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: inspect.lua

Post by Robin » Wed Apr 27, 2011 11:34 am

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.
Help us help you: attach a .love.

User avatar
ishkabible
Party member
Posts: 241
Joined: Sat Oct 23, 2010 7:34 pm
Location: Kansas USA

Re: inspect.lua

Post by ishkabible » Wed Apr 27, 2011 10:43 pm

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

User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: inspect.lua

Post by Robin » Thu Apr 28, 2011 4:37 am

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
Help us help you: attach a .love.

Post Reply

Who is online

Users browsing this forum: No registered users and 3 guests