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