Hey all!
I wrote a function that will come in handy when printing tables in Lua. Normally, when you call print
on a table, it is first tostring
-ed into a memory address and looks something like `table: 000002074CD2C070`. How unhelpful! If only there was a better way…
I’ve created a function, repr
, that works like Python’s repr. It returns a nice, printable representation of Lua values (strings, numbers, bools, and of course tables). It is designed with two goals in mind: (1) be as close as possible to a literal code representation and (2) be as useful as possible during debugging. Check it out at this GitHub repository, or keep reading this post!
local repr = require(3148021300) local myTable = { hello = "world"; score = 5; isCool = true; } print(repr(myTable)) --> {hello = "world", isCool = true, score = 5}
Pretty-print your tables with newlines and indendation by providing a second settings table.
print(repr(myTable, {pretty=true}))Click here to see the pretty-print example output
{ hello = "world", score = 5, isCool = true }
Uses
This function is perfect for learning the structure of complicated tables returned by Roblox functions, such as the recently-released AssetService:GetBundleDetailsAsync. Try this in the command bar:
local t = game:GetService("AssetService"):GetBundleDetailsAsync(492) local repr = require(3148021300) print(repr(t, {pretty=true, sortKeys=true}))Result of AssetService:GetBundleDetailsAsync(492), converted to a string by repr
{ BundleType = "BodyParts", Description = "You definitely have to be an early riser if you're the sun goddess. ", Id = 492, Items = { { Id = 2510233257, Name = "Rthro Fall", Type = "Asset" }, { Id = 2510230574, Name = "Rthro Climb", Type = "Asset" }, { Id = 2510242378, Name = "Rthro Walk", Type = "Asset" }, { Id = 2510240941, Name = "Rthro Swim", Type = "Asset" }, { Id = 2510238627, Name = "Rthro Run", Type = "Asset" }, { Id = 2510236649, Name = "Rthro Jump", Type = "Asset" }, { Id = 2510235063, Name = "Rthro Idle", Type = "Asset" }, { Id = 3141364957, Name = "Erisyphia - Staff", Type = "Asset" }, { Id = 3141351678, Name = "Erisyphia - Right Arm", Type = "Asset" }, { Id = 3141353701, Name = "Erisyphia - Right Leg", Type = "Asset" }, { Id = 3141354966, Name = "Erisyphia - Torso", Type = "Asset" }, { Id = 3141356565, Name = "Erisyphia - Face", Type = "Asset" }, { Id = 2553918762, Name = "Rthro Slender Head", Type = "Asset" }, { Id = 3141358496, Name = "Erisyphia - Hair", Type = "Asset" }, { Id = 3141361350, Name = "Erisyphia - Wings", Type = "Asset" }, { Id = 3141349297, Name = "Erisyphia - Left Arm", Type = "Asset" }, { Id = 3141350577, Name = "Erisyphia - Left Leg", Type = "Asset" }, { Id = 965242919, Name = "Erisyphia", Type = "UserOutfit" } }, Name = "Erisyphia" }
Protip! Install the plugin (see below), and you’ll be able to access _G.repr
in the Command bar!
Features
- Can pretty-print with newlines and indentation (tabs or spaces)
- Works recursively for sub-tables
- Alphabetizes keys automatically
- Keys are properly quoted if they aren’t valid identifiers
- Can print the full name and (optionally) the class of Roblox objects
local repr = require(3148021300) local myTable = { hello = "repr", usefulness = 9001, isEasyToUse = true, array = {"numerical", "arrays", "are", "easy"}, land = workspace["a b c"]["1 2 3"], subTables = { moreInfo = "calls itself recursively to print sub-tables" }, usesToString = {__tostring = function () return "__tostring functions are called automatically" end}, ["$YMBOL$"] = "keys that aren't Lua identifiers are quoted"; [{also = "tables as keys work too";}] = "in case you need that", cyclic = {note = "cyclical tables are printed as just {CYCLIC}"} } -- create a cycle: myTable.cyclic.cyclic = myTable.cyclic local reprSettings = { pretty = false; -- print with \n and indentation? semicolons = false; -- when printing tables, use semicolons (;) instead of commas (,)? sortKeys = true; -- when printing dictionary tables, sort keys alphabetically? spaces = 3; -- when pretty printing, use how many spaces to indent? tabs = false; -- when pretty printing, use tabs instead of spaces? robloxFullName = false; -- when printing Roblox objects, print full name or just name? robloxProperFullName = true; -- when printing Roblox objects, print a proper* full name? robloxClassName = true; -- when printing Roblox objects, also print class name in parens? } print(repr(myTable, reprSettings))
GitHub Repository
Maybe you scrolled past the link the second paragraph, but just in case: repr is available on GitHub. You can download the source code directly here: repr.lua.
Free Model
It’s a ModuleScript named MainModule. You can load it like this:
local repr = require(3148021300)
Plugin
This plugin automatically loads the above free model and stores the function in _G.repr
, very useful when debugging using the Command bar. This is the source code:
if plugin then _G.repr = require(3148021300) end
This enables you to do useful stuff like this within Command bar in Studio:
print(_G.repr{1, 2, 3}) --> {1, 2, 3}
Source Code
Disclaimer: the Free Model may be more up-to-date than this post.
Click here to show the source code for repr v1.1--- repr - Version 1.1 -- Ozzypig - ozzypig.com - http://twitter.com/Ozzypig -- Check out this thread for more info: -- https://devforum.roblox.com/t/repr-function-for-printing-tables/276575 --[[ local repr = require(3148021300) local myTable = { hello = "world", score = 5, isCool = true } print(repr(myTable)) --> {hello = "world", isCool = true, score = 5} ]] local defaultSettings = { pretty = false; robloxFullName = false; robloxProperFullName = true; robloxClassName = true; tabs = false; semicolons = false; spaces = 3; sortKeys = true; } -- lua keywords local keywords = {["and"]=true, ["break"]=true, ["do"]=true, ["else"]=true, ["elseif"]=true, ["end"]=true, ["false"]=true, ["for"]=true, ["function"]=true, ["if"]=true, ["in"]=true, ["local"]=true, ["nil"]=true, ["not"]=true, ["or"]=true, ["repeat"]=true, ["return"]=true, ["then"]=true, ["true"]=true, ["until"]=true, ["while"]=true} local function isLuaIdentifier(str) if type(str) ~= "string" then return false end -- must be nonempty if str:len() == 0 then return false end -- can only contain a-z, A-Z, 0-9 and underscore if str:find("[^%d%a_]") then return false end -- cannot begin with digit if tonumber(str:sub(1, 1)) then return false end -- cannot be keyword if keywords[str] then return false end return true end -- works like Instance:GetFullName(), but invalid Lua identifiers are fixed (e.g. workspace["The Dude"].Humanoid) local function properFullName(object, usePeriod) if object == nil or object == game then return "" end local s = object.Name local usePeriod = true if not isLuaIdentifier(s) then s = ("[%q]"):format(s) usePeriod = false end if not object.Parent or object.Parent == game then return s else return properFullName(object.Parent) .. (usePeriod and "." or "") .. s end end local depth = 0 local shown local INDENT local reprSettings local function repr(value, reprSettings) reprSettings = reprSettings or defaultSettings INDENT = (" "):rep(reprSettings.spaces or defaultSettings.spaces) if reprSettings.tabs then INDENT = "\t" end local v = value --args[1] local tabs = INDENT:rep(depth) if depth == 0 then shown = {} end if type(v) == "string" then return ("%q"):format(v) elseif type(v) == "number" then if v == math.huge then return "math.huge" end if v == -math.huge then return "-math.huge" end return tonumber(v) elseif type(v) == "boolean" then return tostring(v) elseif type(v) == "nil" then return "nil" elseif type(v) == "table" and type(v.__tostring) == "function" then return tostring(v.__tostring(v)) elseif type(v) == "table" and getmetatable(v) and type(getmetatable(v).__tostring) == "function" then return tostring(getmetatable(v).__tostring(v)) elseif type(v) == "table" then if shown[v] then return "{CYCLIC}" end shown[v] = true local str = "{" .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or "") local isArray = true for k, v in pairs(v) do if type(k) ~= "number" then isArray = false break end end if isArray then for i = 1, #v do if i ~= 1 then str = str .. (reprSettings.semicolons and ";" or ",") .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ") end depth = depth + 1 str = str .. repr(v[i], reprSettings) depth = depth - 1 end else local keyOrder = {} local keyValueStrings = {} for k, v in pairs(v) do depth = depth + 1 local kStr = isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]") local vStr = repr(v, reprSettings) --[[str = str .. ("%s = %s"):format( isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]"), repr(v, reprSettings) )]] table.insert(keyOrder, kStr) keyValueStrings[kStr] = vStr depth = depth - 1 end if reprSettings.sortKeys then table.sort(keyOrder) end local first = true for _, kStr in pairs(keyOrder) do if not first then str = str .. (reprSettings.semicolons and ";" or ",") .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ") end str = str .. ("%s = %s"):format(kStr, keyValueStrings[kStr]) first = false end end shown[v] = false if reprSettings.pretty then str = str .. "\n" .. tabs end str = str .. "}" return str elseif typeof then -- Check Roblox types if typeof(v) == "Instance" then return (reprSettings.robloxFullName and (reprSettings.robloxProperFullName and properFullName(v) or v:GetFullName()) or v.Name) .. (reprSettings.robloxClassName and ((" (%s)"):format(v.ClassName)) or "") elseif typeof(v) == "Axes" then local s = {} if v.X then table.insert(s, repr(Enum.Axis.X, reprSettings)) end if v.Y then table.insert(s, repr(Enum.Axis.Y, reprSettings)) end if v.Z then table.insert(s, repr(Enum.Axis.Z, reprSettings)) end return ("Axes.new(%s)"):format(table.concat(s, ", ")) elseif typeof(v) == "BrickColor" then return ("BrickColor.new(%q)"):format(v.Name) elseif typeof(v) == "CFrame" then return ("CFrame.new(%s)"):format(table.concat({v:GetComponents()}, ", ")) elseif typeof(v) == "Color3" then return ("Color3.new(%d, %d, %d)"):format(v.r, v.g, v.b) elseif typeof(v) == "ColorSequence" then if #v.Keypoints > 2 then return ("ColorSequence.new(%s)"):format(repr(v.Keypoints, reprSettings)) else if v.Keypoints[1].Value == v.Keypoints[2].Value then return ("ColorSequence.new(%s)"):format(repr(v.Keypoints[1].Value, reprSettings)) else return ("ColorSequence.new(%s, %s)"):format( repr(v.Keypoints[1].Value, reprSettings), repr(v.Keypoints[2].Value, reprSettings) ) end end elseif typeof(v) == "ColorSequenceKeypoint" then return ("ColorSequenceKeypoint.new(%d, %s)"):format(v.Time, repr(v.Value, reprSettings)) elseif typeof(v) == "DockWidgetPluginGuiInfo" then return ("DockWidgetPluginGuiInfo.new(%s, %s, %s, %s, %s, %s, %s)"):format( repr(v.InitialDockState, reprSettings), repr(v.InitialEnabled, reprSettings), repr(v.InitialEnabledShouldOverrideRestore, reprSettings), repr(v.FloatingXSize, reprSettings), repr(v.FloatingYSize, reprSettings), repr(v.MinWidth, reprSettings), repr(v.MinHeight, reprSettings) ) elseif typeof(v) == "Enums" then return "Enums" elseif typeof(v) == "Enum" then return ("Enum.%s"):format(tostring(v)) elseif typeof(v) == "EnumItem" then return ("Enum.%s.%s"):format(tostring(v.EnumType), v.Name) elseif typeof(v) == "Faces" then local s = {} for _, enumItem in pairs(Enum.NormalId:GetEnumItems()) do if v[enumItem.Name] then table.insert(s, repr(enumItem, reprSettings)) end end return ("Faces.new(%s)"):format(table.concat(s, ", ")) elseif typeof(v) == "NumberRange" then if v.Min == v.Max then return ("NumberRange.new(%d)"):format(v.Min) else return ("NumberRange.new(%d, %d)"):format(v.Min, v.Max) end elseif typeof(v) == "NumberSequence" then if #v.Keypoints > 2 then return ("NumberSequence.new(%s)"):format(repr(v.Keypoints, reprSettings)) else if v.Keypoints[1].Value == v.Keypoints[2].Value then return ("NumberSequence.new(%d)"):format(v.Keypoints[1].Value) else return ("NumberSequence.new(%d, %d)"):format(v.Keypoints[1].Value, v.Keypoints[2].Value) end end elseif typeof(v) == "NumberSequenceKeypoint" then if v.Envelope ~= 0 then return ("NumberSequenceKeypoint.new(%d, %d, %d)"):format(v.Time, v.Value, v.Envelope) else return ("NumberSequenceKeypoint.new(%d, %d)"):format(v.Time, v.Value) end elseif typeof(v) == "PathWaypoint" then return ("PathWaypoint.new(%s, %s)"):format( repr(v.Position, reprSettings), repr(v.Action, reprSettings) ) elseif typeof(v) == "PhysicalProperties" then return ("PhysicalProperties.new(%d, %d, %d, %d, %d)"):format( v.Density, v.Friction, v.Elasticity, v.FrictionWeight, v.ElasticityWeight ) elseif typeof(v) == "Random" then return "<Random>" elseif typeof(v) == "Ray" then return ("Ray.new(%s, %s)"):format( repr(v.Origin, reprSettings), repr(v.Direction, reprSettings) ) elseif typeof(v) == "RBXScriptConnection" then return "<RBXScriptConnection>" elseif typeof(v) == "RBXScriptSignal" then return "<RBXScriptSignal>" elseif typeof(v) == "Rect" then return ("Rect.new(%d, %d, %d, %d)"):format( v.Min.X, v.Min.Y, v.Max.X, v.Max.Y ) elseif typeof(v) == "Region3" then local min = v.CFrame.p + v.Size * -.5 local max = v.CFrame.p + v.Size * .5 return ("Region3.new(%s, %s)"):format( repr(min, reprSettings), repr(max, reprSettings) ) elseif typeof(v) == "Region3int16" then return ("Region3int16.new(%s, %s)"):format( repr(v.Min, reprSettings), repr(v.Max, reprSettings) ) elseif typeof(v) == "TweenInfo" then return ("TweenInfo.new(%d, %s, %s, %d, %s, %d)"):format( v.Time, repr(v.EasingStyle, reprSettings), repr(v.EasingDirection, reprSettings), v.RepeatCount, repr(v.Reverses, reprSettings), v.DelayTime ) elseif typeof(v) == "UDim" then return ("UDim.new(%d, %d)"):format( v.Scale, v.Offset ) elseif typeof(v) == "UDim2" then return ("UDim2.new(%d, %d, %d, %d)"):format( v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset ) elseif typeof(v) == "Vector2" then return ("Vector2.new(%d, %d"):format(v.X, v.Y) elseif typeof(v) == "Vector2int16" then return ("Vector2int16.new(%d, %d)"):format(v.X, v.Y) elseif typeof(v) == "Vector3" then return ("Vector3.new(%d, %d, %d)"):format(v.X, v.Y, v.Z) elseif typeof(v) == "Vector3int16" then return ("Vector3int16.new(%d, %d, %d)"):format(v.X, v.Y, v.Z) else return "<Roblox:" .. typeof(v) .. ">" end else return "<" .. type(v) .. ">" end end return repr
Version History
- 5 May 2019 – v1.0 Initial release
- 6 May 2019 – v1.1 Now supports all Roblox data types (Vector3, Color3, etc.)
Portability
This function won’t break in non-Roblox Lua environments, like LÖVE, Gideros, Gary’s Mod etc. Hello to our fellow Lua programmers from the great beyond 🙂
Licensing
I license this work (the Lua code, model, and plugin) under the WTFPL. Go nuts. Let me know if it helped you make something cool. Please don’t to use it for world domination (it’s where I keep my stuff).
Devforum Thread
I’ve posted this to Roblox’s Developer Forum. You can leave your questions, comments and concerns over there, or send them to me on Twitter.