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’s short for representation, and it returns a nice, printable representation of Lua values (strings, numbers, bools, and of course tables). It is designed with two goals in mind:
- Be as close as possible to a literal code representation
- Be as useful as possible during debugging
Check it out:
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}))
Example pretty-print 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 from the link 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
- Supports all Roblox data types
Click here for an in-depth example that shows off all the bells and whistles!
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))
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. It also exposes _G.print_repr
, which sends the result to print
before returning it.
_G.print_repr{1, 2, 3} --> {1, 2, 3}
print(_G.repr{1, 2, 3}) --> {1, 2, 3}
Click here to view the repr plugin source code
assert(plugin, "This should be run as a plugin!")
if plugin and game:GetService("RunService"):IsServer() then
local repr = require(script.Parent)
_G.repr = repr
_G.print_repr = function (...)
local retVals = {repr(...)}
print(unpack(retVals))
return unpack(retVals)
end
end
Repository on GitHub
Big boys and girls use proper source control! If you’re a big boy or girl, you might like to clone this repository:
$ git clone https://github.com/Ozzypig/repr
Raw source code
Note: the Free Model or GitHub repo may be more up-to-date than this thread.
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…)
- 12 December 2019 - No change, but the plugin now exposes
_G.print_repr
, which is the same as_G.repr
but will print the result before returning it.
Portability
This function won’t break in non-Roblox Lua environments, as it safely checks for the Roblox-exclusive typeof
function. So feel free to use it in LÖVE, Gideros, Factorio, 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 use it for world domination (that’s where I keep all my stuff).