inspect.lua 9.3 KB

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