inspect.lua 9.5 KB

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