inspect.lua 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. local _tl_compat
  2. if (tonumber((_VERSION or ""):match "[%d.]*$") or 0) < 5.3 then
  3. local p, m = pcall(require, "compat53.module")
  4. if p then _tl_compat = m end
  5. end
  6. local math = _tl_compat and _tl_compat.math or math
  7. local string = _tl_compat and _tl_compat.string or string
  8. local table = _tl_compat and _tl_compat.table or table
  9. local inspect = { Options = {} }
  10. inspect._VERSION = "inspect.lua 3.1.0"
  11. inspect._URL = "http://github.com/kikito/inspect.lua"
  12. inspect._DESCRIPTION = "human-readable representations of tables"
  13. inspect._LICENSE = [[
  14. MIT LICENSE
  15. Copyright (c) 2022 Enrique García Cota
  16. Permission is hereby granted, free of charge, to any person obtaining a
  17. copy of this software and associated documentation files (the
  18. "Software"), to deal in the Software without restriction, including
  19. without limitation the rights to use, copy, modify, merge, publish,
  20. distribute, sublicense, and/or sell copies of the Software, and to
  21. permit persons to whom the Software is furnished to do so, subject to
  22. the following conditions:
  23. The above copyright notice and this permission notice shall be included
  24. in all copies or substantial portions of the Software.
  25. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  26. OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  27. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  28. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  29. CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  30. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  31. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  32. ]]
  33. inspect.KEY = setmetatable({}, {
  34. __tostring = function() return "inspect.KEY" end,
  35. })
  36. inspect.METATABLE = setmetatable({}, {
  37. __tostring = function() return "inspect.METATABLE" end,
  38. })
  39. local tostring = tostring
  40. local rep = string.rep
  41. local match = string.match
  42. local char = string.char
  43. local gsub = string.gsub
  44. local fmt = string.format
  45. local _rawget
  46. if rawget then
  47. _rawget = rawget
  48. else
  49. _rawget = function(t, k) return t[k] end
  50. end
  51. local function rawpairs(t) return next, t, nil end
  52. local function smartQuote(str)
  53. if match(str, '"') and not match(str, "'") then return "'" .. str .. "'" end
  54. return '"' .. gsub(str, '"', '\\"') .. '"'
  55. end
  56. local shortControlCharEscapes = {
  57. ["\a"] = "\\a",
  58. ["\b"] = "\\b",
  59. ["\f"] = "\\f",
  60. ["\n"] = "\\n",
  61. ["\r"] = "\\r",
  62. ["\t"] = "\\t",
  63. ["\v"] = "\\v",
  64. ["\127"] = "\\127",
  65. }
  66. local longControlCharEscapes = { ["\127"] = "\127" }
  67. for i = 0, 31 do
  68. local ch = char(i)
  69. if not shortControlCharEscapes[ch] then
  70. shortControlCharEscapes[ch] = "\\" .. i
  71. longControlCharEscapes[ch] = fmt("\\%03d", i)
  72. end
  73. end
  74. local function escape(str)
  75. return (
  76. gsub(
  77. gsub(gsub(str, "\\", "\\\\"), "(%c)%f[0-9]", longControlCharEscapes),
  78. "%c",
  79. shortControlCharEscapes
  80. )
  81. )
  82. end
  83. local luaKeywords = {
  84. ["and"] = true,
  85. ["break"] = true,
  86. ["do"] = true,
  87. ["else"] = true,
  88. ["elseif"] = true,
  89. ["end"] = true,
  90. ["false"] = true,
  91. ["for"] = true,
  92. ["function"] = true,
  93. ["goto"] = true,
  94. ["if"] = true,
  95. ["in"] = true,
  96. ["local"] = true,
  97. ["nil"] = true,
  98. ["not"] = true,
  99. ["or"] = true,
  100. ["repeat"] = true,
  101. ["return"] = true,
  102. ["then"] = true,
  103. ["true"] = true,
  104. ["until"] = true,
  105. ["while"] = true,
  106. }
  107. local function isIdentifier(str)
  108. return type(str) == "string"
  109. and not not str:match "^[_%a][_%a%d]*$"
  110. and not luaKeywords[str]
  111. end
  112. local flr = math.floor
  113. local function isSequenceKey(k, sequenceLength)
  114. return type(k) == "number" and flr(k) == k and 1 <= k and k <= sequenceLength
  115. end
  116. local defaultTypeOrders = {
  117. ["number"] = 1,
  118. ["boolean"] = 2,
  119. ["string"] = 3,
  120. ["table"] = 4,
  121. ["function"] = 5,
  122. ["userdata"] = 6,
  123. ["thread"] = 7,
  124. }
  125. local function sortKeys(a, b)
  126. local ta, tb = type(a), type(b)
  127. if ta == tb and (ta == "string" or ta == "number") then return a < b end
  128. local dta = defaultTypeOrders[ta] or 100
  129. local dtb = defaultTypeOrders[tb] or 100
  130. return dta == dtb and ta < tb or dta < dtb
  131. end
  132. local function getKeys(t)
  133. local seqLen = 1
  134. while _rawget(t, seqLen) ~= nil do
  135. seqLen = seqLen + 1
  136. end
  137. seqLen = seqLen - 1
  138. local keys, keysLen = {}, 0
  139. for k in rawpairs(t) do
  140. if not isSequenceKey(k, seqLen) then
  141. keysLen = keysLen + 1
  142. keys[keysLen] = k
  143. end
  144. end
  145. table.sort(keys, sortKeys)
  146. return keys, keysLen, seqLen
  147. end
  148. local function countCycles(x, cycles)
  149. if type(x) == "table" then
  150. if cycles[x] then
  151. cycles[x] = cycles[x] + 1
  152. else
  153. cycles[x] = 1
  154. for k, v in rawpairs(x) do
  155. countCycles(k, cycles)
  156. countCycles(v, cycles)
  157. end
  158. countCycles(getmetatable(x), cycles)
  159. end
  160. end
  161. end
  162. local function makePath(path, a, b)
  163. local newPath = {}
  164. local len = #path
  165. for i = 1, len do
  166. newPath[i] = path[i]
  167. end
  168. newPath[len + 1] = a
  169. newPath[len + 2] = b
  170. return newPath
  171. end
  172. local function processRecursive(process, item, path, visited)
  173. if item == nil then return nil end
  174. if visited[item] then return visited[item] end
  175. local processed = process(item, path)
  176. if type(processed) == "table" then
  177. local processedCopy = {}
  178. visited[item] = processedCopy
  179. local processedKey
  180. for k, v in rawpairs(processed) do
  181. processedKey =
  182. processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
  183. if processedKey ~= nil then
  184. processedCopy[processedKey] =
  185. processRecursive(process, v, makePath(path, processedKey), visited)
  186. end
  187. end
  188. local mt = processRecursive(
  189. process,
  190. getmetatable(processed),
  191. makePath(path, inspect.METATABLE),
  192. visited
  193. )
  194. if type(mt) ~= "table" then mt = nil end
  195. setmetatable(processedCopy, mt)
  196. processed = processedCopy
  197. end
  198. return processed
  199. end
  200. local function puts(buf, str)
  201. buf.n = buf.n + 1
  202. buf[buf.n] = str
  203. end
  204. local Inspector = {}
  205. local Inspector_mt = { __index = Inspector }
  206. local function tabify(inspector)
  207. puts(
  208. inspector.buf,
  209. inspector.newline .. rep(inspector.indent, inspector.level)
  210. )
  211. end
  212. function Inspector:getId(v)
  213. local id = self.ids[v]
  214. local ids = self.ids
  215. if not id then
  216. local tv = type(v)
  217. id = (ids[tv] or 0) + 1
  218. ids[v], ids[tv] = id, id
  219. end
  220. return tostring(id)
  221. end
  222. function Inspector:putValue(v)
  223. local buf = self.buf
  224. local tv = type(v)
  225. if tv == "string" then
  226. puts(buf, smartQuote(escape(v)))
  227. elseif tv == "userdata" then
  228. puts(buf, dump(v):gsub("\n", "\n "))
  229. elseif
  230. tv == "number"
  231. or tv == "boolean"
  232. or tv == "nil"
  233. or tv == "cdata"
  234. or tv == "ctype"
  235. then
  236. puts(buf, tostring(v))
  237. elseif tv == "table" and not self.ids[v] then
  238. local t = v
  239. if t == inspect.KEY or t == inspect.METATABLE then
  240. puts(buf, tostring(t))
  241. elseif self.level >= self.depth then
  242. puts(buf, "{...}")
  243. else
  244. if self.cycles[t] > 1 then puts(buf, fmt("<%d>", self:getId(t))) end
  245. local keys, keysLen, seqLen = getKeys(t)
  246. puts(buf, "{")
  247. self.level = self.level + 1
  248. for i = 1, seqLen + keysLen do
  249. if i > 1 then puts(buf, ",") end
  250. if i <= seqLen then
  251. puts(buf, " ")
  252. self:putValue(t[i])
  253. else
  254. local k = keys[i - seqLen]
  255. tabify(self)
  256. if isIdentifier(k) then
  257. puts(buf, k)
  258. else
  259. puts(buf, "[")
  260. self:putValue(k)
  261. puts(buf, "]")
  262. end
  263. puts(buf, " = ")
  264. self:putValue(t[k])
  265. end
  266. end
  267. local mt = getmetatable(t)
  268. if type(mt) == "table" then
  269. if seqLen + keysLen > 0 then puts(buf, ",") end
  270. tabify(self)
  271. puts(buf, "<metatable> = ")
  272. self:putValue(mt)
  273. end
  274. self.level = self.level - 1
  275. if keysLen > 0 or type(mt) == "table" then
  276. tabify(self)
  277. elseif seqLen > 0 then
  278. puts(buf, " ")
  279. end
  280. puts(buf, "}")
  281. end
  282. else
  283. puts(buf, fmt("<%s %d>", tv, self:getId(v)))
  284. end
  285. end
  286. function inspect.inspect(root, options)
  287. options = options or {}
  288. local depth = options.depth or math.huge
  289. local newline = options.newline or "\n"
  290. local indent = options.indent or " "
  291. local process = options.process
  292. if process then root = processRecursive(process, root, {}, {}) end
  293. local cycles = {}
  294. countCycles(root, cycles)
  295. local inspector = setmetatable({
  296. buf = { n = 0 },
  297. ids = {},
  298. cycles = cycles,
  299. depth = depth,
  300. level = 0,
  301. newline = newline,
  302. indent = indent,
  303. }, Inspector_mt)
  304. inspector:putValue(root)
  305. return table.concat(inspector.buf)
  306. end
  307. setmetatable(inspect, {
  308. __call = function(_, root, options) return inspect.inspect(root, options) end,
  309. })
  310. return inspect