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