inspect.lua 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. local inspect ={
  2. _VERSION = 'inspect.lua 3.0.0',
  3. _URL = 'http://github.com/kikito/inspect.lua',
  4. _DESCRIPTION = 'human-readable representations of tables',
  5. _LICENSE = [[
  6. MIT LICENSE
  7. Copyright (c) 2013 Enrique García Cota
  8. Permission is hereby granted, free of charge, to any person obtaining a
  9. copy of this software and associated documentation files (the
  10. "Software"), to deal in the Software without restriction, including
  11. without limitation the rights to use, copy, modify, merge, publish,
  12. distribute, sublicense, and/or sell copies of the Software, and to
  13. permit persons to whom the Software is furnished to do so, subject to
  14. the following conditions:
  15. The above copyright notice and this permission notice shall be included
  16. in all copies or substantial portions of the Software.
  17. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  18. OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  19. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  20. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  21. CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  22. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  23. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  24. ]]
  25. }
  26. inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
  27. inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
  28. -- Apostrophizes the string if it has quotes, but not aphostrophes
  29. -- Otherwise, it returns a regular quoted string
  30. local function smartQuote(str)
  31. if str:match('"') and not str:match("'") then
  32. return "'" .. str .. "'"
  33. end
  34. return '"' .. str:gsub('"', '\\"') .. '"'
  35. end
  36. local controlCharsTranslation = {
  37. ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
  38. ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
  39. }
  40. local function escape(str)
  41. local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation)
  42. return result
  43. end
  44. local function isIdentifier(str)
  45. return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
  46. end
  47. local function isSequenceKey(k, length)
  48. return type(k) == 'number'
  49. and 1 <= k
  50. and k <= length
  51. and math.floor(k) == k
  52. end
  53. local defaultTypeOrders = {
  54. ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
  55. ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
  56. }
  57. local function sortKeys(a, b)
  58. local ta, tb = type(a), type(b)
  59. -- strings and numbers are sorted numerically/alphabetically
  60. if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
  61. local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
  62. -- Two default types are compared according to the defaultTypeOrders table
  63. if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
  64. elseif dta then return true -- default types before custom ones
  65. elseif dtb then return false -- custom types after default ones
  66. end
  67. -- custom types are sorted out alphabetically
  68. return ta < tb
  69. end
  70. local function getNonSequentialKeys(t)
  71. local keys, length = {}, #t
  72. for k,_ in pairs(t) do
  73. if not isSequenceKey(k, length) then table.insert(keys, k) end
  74. end
  75. table.sort(keys, sortKeys)
  76. return keys
  77. end
  78. local function getToStringResultSafely(t, mt)
  79. local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
  80. local str, ok
  81. if type(__tostring) == 'function' then
  82. ok, str = pcall(__tostring, t)
  83. str = ok and str or 'error: ' .. tostring(str)
  84. end
  85. if type(str) == 'string' and #str > 0 then return str end
  86. end
  87. local maxIdsMetaTable = {
  88. __index = function(self, typeName)
  89. rawset(self, typeName, 0)
  90. return 0
  91. end
  92. }
  93. local idsMetaTable = {
  94. __index = function (self, typeName)
  95. local col = setmetatable({}, {__mode = "kv"})
  96. rawset(self, typeName, col)
  97. return col
  98. end
  99. }
  100. local function countTableAppearances(t, tableAppearances)
  101. tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"})
  102. if type(t) == 'table' then
  103. if not tableAppearances[t] then
  104. tableAppearances[t] = 1
  105. for k,v in pairs(t) do
  106. countTableAppearances(k, tableAppearances)
  107. countTableAppearances(v, tableAppearances)
  108. end
  109. countTableAppearances(getmetatable(t), tableAppearances)
  110. else
  111. tableAppearances[t] = tableAppearances[t] + 1
  112. end
  113. end
  114. return tableAppearances
  115. end
  116. local copySequence = function(s)
  117. local copy, len = {}, #s
  118. for i=1, len do copy[i] = s[i] end
  119. return copy, len
  120. end
  121. local function makePath(path, ...)
  122. local keys = {...}
  123. local newPath, len = copySequence(path)
  124. for i=1, #keys do
  125. newPath[len + i] = keys[i]
  126. end
  127. return newPath
  128. end
  129. local function processRecursive(process, item, path)
  130. if item == nil then return nil end
  131. local processed = process(item, path)
  132. if type(processed) == 'table' then
  133. local processedCopy = {}
  134. local processedKey
  135. for k,v in pairs(processed) do
  136. processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY))
  137. if processedKey ~= nil then
  138. processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey))
  139. end
  140. end
  141. local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE))
  142. setmetatable(processedCopy, mt)
  143. processed = processedCopy
  144. end
  145. return processed
  146. end
  147. -------------------------------------------------------------------
  148. local Inspector = {}
  149. local Inspector_mt = {__index = Inspector}
  150. function Inspector:puts(...)
  151. local args = {...}
  152. local buffer = self.buffer
  153. local len = #buffer
  154. for i=1, #args do
  155. len = len + 1
  156. buffer[len] = tostring(args[i])
  157. end
  158. end
  159. function Inspector:down(f)
  160. self.level = self.level + 1
  161. f()
  162. self.level = self.level - 1
  163. end
  164. function Inspector:tabify()
  165. self:puts(self.newline, string.rep(self.indent, self.level))
  166. end
  167. function Inspector:alreadyVisited(v)
  168. return self.ids[type(v)][v] ~= nil
  169. end
  170. function Inspector:getId(v)
  171. local tv = type(v)
  172. local id = self.ids[tv][v]
  173. if not id then
  174. id = self.maxIds[tv] + 1
  175. self.maxIds[tv] = id
  176. self.ids[tv][v] = id
  177. end
  178. return id
  179. end
  180. function Inspector:putKey(k)
  181. if isIdentifier(k) then return self:puts(k) end
  182. self:puts("[")
  183. self:putValue(k)
  184. self:puts("]")
  185. end
  186. function Inspector:putTable(t)
  187. if t == inspect.KEY or t == inspect.METATABLE then
  188. self:puts(tostring(t))
  189. elseif self:alreadyVisited(t) then
  190. self:puts('<table ', self:getId(t), '>')
  191. elseif self.level >= self.depth then
  192. self:puts('{...}')
  193. else
  194. if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
  195. local nonSequentialKeys = getNonSequentialKeys(t)
  196. local length = #t
  197. local mt = getmetatable(t)
  198. local toStringResult = getToStringResultSafely(t, mt)
  199. self:puts('{')
  200. self:down(function()
  201. if toStringResult then
  202. self:puts(' -- ', escape(toStringResult))
  203. if length >= 1 then self:tabify() end
  204. end
  205. local count = 0
  206. for i=1, length do
  207. if count > 0 then self:puts(',') end
  208. self:puts(' ')
  209. self:putValue(t[i])
  210. count = count + 1
  211. end
  212. for _,k in ipairs(nonSequentialKeys) do
  213. if count > 0 then self:puts(',') end
  214. self:tabify()
  215. self:putKey(k)
  216. self:puts(' = ')
  217. self:putValue(t[k])
  218. count = count + 1
  219. end
  220. if mt then
  221. if count > 0 then self:puts(',') end
  222. self:tabify()
  223. self:puts('<metatable> = ')
  224. self:putValue(mt)
  225. end
  226. end)
  227. if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing }
  228. self:tabify()
  229. elseif length > 0 then -- array tables have one extra space before closing }
  230. self:puts(' ')
  231. end
  232. self:puts('}')
  233. end
  234. end
  235. function Inspector:putValue(v)
  236. local tv = type(v)
  237. if tv == 'string' then
  238. self:puts(smartQuote(escape(v)))
  239. elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
  240. self:puts(tostring(v))
  241. elseif tv == 'table' then
  242. self:putTable(v)
  243. else
  244. self:puts('<',tv,' ',self:getId(v),'>')
  245. end
  246. end
  247. -------------------------------------------------------------------
  248. function inspect.inspect(root, options)
  249. options = options or {}
  250. local depth = options.depth or math.huge
  251. local newline = options.newline or '\n'
  252. local indent = options.indent or ' '
  253. local process = options.process
  254. if process then
  255. root = processRecursive(process, root, {})
  256. end
  257. local inspector = setmetatable({
  258. depth = depth,
  259. buffer = {},
  260. level = 0,
  261. ids = setmetatable({}, idsMetaTable),
  262. maxIds = setmetatable({}, maxIdsMetaTable),
  263. newline = newline,
  264. indent = indent,
  265. tableAppearances = countTableAppearances(root)
  266. }, Inspector_mt)
  267. inspector:putValue(root)
  268. return table.concat(inspector.buffer)
  269. end
  270. setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
  271. return inspect