inspect.lua 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. local inspect ={
  2. _VERSION = 'inspect.lua 3.0.2',
  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. -- returns the length of a table, ignoring __len (if it exists)
  29. local rawlen = _G.rawlen or function(t) return #t end
  30. -- Apostrophizes the string if it has quotes, but not aphostrophes
  31. -- Otherwise, it returns a regular quoted string
  32. local function smartQuote(str)
  33. if str:match('"') and not str:match("'") then
  34. return "'" .. str .. "'"
  35. end
  36. return '"' .. str:gsub('"', '\\"') .. '"'
  37. end
  38. local controlCharsTranslation = {
  39. ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
  40. ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
  41. }
  42. local function escape(str)
  43. local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation)
  44. return result
  45. end
  46. local function isIdentifier(str)
  47. return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
  48. end
  49. local function isSequenceKey(k, length)
  50. return type(k) == 'number'
  51. and 1 <= k
  52. and k <= length
  53. and math.floor(k) == k
  54. end
  55. local defaultTypeOrders = {
  56. ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
  57. ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
  58. }
  59. local function sortKeys(a, b)
  60. local ta, tb = type(a), type(b)
  61. -- strings and numbers are sorted numerically/alphabetically
  62. if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
  63. local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
  64. -- Two default types are compared according to the defaultTypeOrders table
  65. if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
  66. elseif dta then return true -- default types before custom ones
  67. elseif dtb then return false -- custom types after default ones
  68. end
  69. -- custom types are sorted out alphabetically
  70. return ta < tb
  71. end
  72. local function getNonSequentialKeys(t)
  73. local keys, length = {}, rawlen(t)
  74. for k,_ in pairs(t) do
  75. if not isSequenceKey(k, length) then table.insert(keys, k) end
  76. end
  77. table.sort(keys, sortKeys)
  78. return keys
  79. end
  80. local function getToStringResultSafely(t, mt)
  81. local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
  82. local str, ok
  83. if type(__tostring) == 'function' then
  84. ok, str = pcall(__tostring, t)
  85. str = ok and str or 'error: ' .. tostring(str)
  86. end
  87. if type(str) == 'string' and #str > 0 then return str end
  88. end
  89. local maxIdsMetaTable = {
  90. __index = function(self, typeName)
  91. rawset(self, typeName, 0)
  92. return 0
  93. end
  94. }
  95. local idsMetaTable = {
  96. __index = function (self, typeName)
  97. local col = {}
  98. rawset(self, typeName, col)
  99. return col
  100. end
  101. }
  102. local function countTableAppearances(t, tableAppearances)
  103. tableAppearances = tableAppearances or {}
  104. if type(t) == 'table' then
  105. if not tableAppearances[t] then
  106. tableAppearances[t] = 1
  107. for k,v in pairs(t) do
  108. countTableAppearances(k, tableAppearances)
  109. countTableAppearances(v, tableAppearances)
  110. end
  111. countTableAppearances(getmetatable(t), tableAppearances)
  112. else
  113. tableAppearances[t] = tableAppearances[t] + 1
  114. end
  115. end
  116. return tableAppearances
  117. end
  118. local copySequence = function(s)
  119. local copy, len = {}, #s
  120. for i=1, len do copy[i] = s[i] end
  121. return copy, len
  122. end
  123. local function makePath(path, ...)
  124. local keys = {...}
  125. local newPath, len = copySequence(path)
  126. for i=1, #keys do
  127. newPath[len + i] = keys[i]
  128. end
  129. return newPath
  130. end
  131. local function processRecursive(process, item, path)
  132. if item == nil then return nil end
  133. local processed = process(item, path)
  134. if type(processed) == 'table' then
  135. local processedCopy = {}
  136. local processedKey
  137. for k,v in pairs(processed) do
  138. processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY))
  139. if processedKey ~= nil then
  140. processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey))
  141. end
  142. end
  143. local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE))
  144. setmetatable(processedCopy, mt)
  145. processed = processedCopy
  146. end
  147. return processed
  148. end
  149. -------------------------------------------------------------------
  150. local Inspector = {}
  151. local Inspector_mt = {__index = Inspector}
  152. function Inspector:puts(...)
  153. local args = {...}
  154. local buffer = self.buffer
  155. local len = #buffer
  156. for i=1, #args do
  157. len = len + 1
  158. buffer[len] = tostring(args[i])
  159. end
  160. end
  161. function Inspector:down(f)
  162. self.level = self.level + 1
  163. f()
  164. self.level = self.level - 1
  165. end
  166. function Inspector:tabify()
  167. self:puts(self.newline, string.rep(self.indent, self.level))
  168. end
  169. function Inspector:alreadyVisited(v)
  170. return self.ids[type(v)][v] ~= nil
  171. end
  172. function Inspector:getId(v)
  173. local tv = type(v)
  174. local id = self.ids[tv][v]
  175. if not id then
  176. id = self.maxIds[tv] + 1
  177. self.maxIds[tv] = id
  178. self.ids[tv][v] = id
  179. end
  180. return id
  181. end
  182. function Inspector:putKey(k)
  183. if isIdentifier(k) then return self:puts(k) end
  184. self:puts("[")
  185. self:putValue(k)
  186. self:puts("]")
  187. end
  188. function Inspector:putTable(t)
  189. if t == inspect.KEY or t == inspect.METATABLE then
  190. self:puts(tostring(t))
  191. elseif self:alreadyVisited(t) then
  192. self:puts('<table ', self:getId(t), '>')
  193. elseif self.level >= self.depth then
  194. self:puts('{...}')
  195. else
  196. if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
  197. local nonSequentialKeys = getNonSequentialKeys(t)
  198. local length = rawlen(t)
  199. local mt = getmetatable(t)
  200. local toStringResult = getToStringResultSafely(t, mt)
  201. self:puts('{')
  202. self:down(function()
  203. if toStringResult then
  204. self:puts(' -- ', escape(toStringResult))
  205. if length >= 1 then self:tabify() end
  206. end
  207. local count = 0
  208. for i=1, length do
  209. if count > 0 then self:puts(',') end
  210. self:puts(' ')
  211. self:putValue(t[i])
  212. count = count + 1
  213. end
  214. for _,k in ipairs(nonSequentialKeys) do
  215. if count > 0 then self:puts(',') end
  216. self:tabify()
  217. self:putKey(k)
  218. self:puts(' = ')
  219. self:putValue(t[k])
  220. count = count + 1
  221. end
  222. if mt then
  223. if count > 0 then self:puts(',') end
  224. self:tabify()
  225. self:puts('<metatable> = ')
  226. self:putValue(mt)
  227. end
  228. end)
  229. if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing }
  230. self:tabify()
  231. elseif length > 0 then -- array tables have one extra space before closing }
  232. self:puts(' ')
  233. end
  234. self:puts('}')
  235. end
  236. end
  237. function Inspector:putValue(v)
  238. local tv = type(v)
  239. if tv == 'string' then
  240. self:puts(smartQuote(escape(v)))
  241. elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
  242. self:puts(tostring(v))
  243. elseif tv == 'table' then
  244. self:putTable(v)
  245. else
  246. if tv ~= "function" then
  247. local ok, str = pcall(tostring, v)
  248. -- if type(v.__tostring) == "function" then
  249. -- local ok, str = pcall(v.__tostring, v)
  250. if ok then
  251. self:puts(tostring(v))
  252. return
  253. end
  254. -- end
  255. end
  256. self:puts('<',tv,' ',self:getId(v),'>')
  257. end
  258. end
  259. -------------------------------------------------------------------
  260. function inspect.inspect(root, options)
  261. options = options or {}
  262. local depth = options.depth or math.huge
  263. local newline = options.newline or '\n'
  264. local indent = options.indent or ' '
  265. local process = options.process
  266. if process then
  267. root = processRecursive(process, root, {})
  268. end
  269. local inspector = setmetatable({
  270. depth = depth,
  271. buffer = {},
  272. level = 0,
  273. ids = setmetatable({}, idsMetaTable),
  274. maxIds = setmetatable({}, maxIdsMetaTable),
  275. newline = newline,
  276. indent = indent,
  277. tableAppearances = countTableAppearances(root)
  278. }, Inspector_mt)
  279. inspector:putValue(root)
  280. return table.concat(inspector.buffer)
  281. end
  282. setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
  283. return inspect