Repr – function for printing tables

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:

  1. Be as close as possible to a literal code representation
  2. 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 :wave: :slight_smile: :+1:

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).

Oh, and… I’ve also posted this to my website, in case anyone wants to read this information there.

141 Likes

The Free Model link takes you to a 404 error. It needs to be replaced with https://www.roblox.com/library/3148021300/repr or https://www.roblox.com/catalog/3148021300.

I’ll probably be using this quite a lot, especially for understanding functions that return advanced results. Thank you for the release.

2 Likes

Thank you for creating this! Sometimes I need to see what a table contains, looping through it with a generic loop and printing the descendants is tiring, especially when a table contains another one inside it.

1 Like

Very useful, good job! I will definitely be using this within future projects.

1 Like

It’s interesting that this goes out of its way to canonize the infinities (math.huge/-math.huge) but doesn’t do the same for NaN.

The plugin also seems to not work for me. It’s not setting the global.

Just updated the plugin - try one more time. The original had an issue where the plugin was running on client (accurate play solo) and spitting out an error. The band-aid fix had an issue as well, but I’ve re-checked it and everything should work as intended.

To be clear, the plugin won’t set the global if RunService:IsServer() returns false because only the server can require(assetId).

I may have neglected this detail. Sorry!

Is it just me being a noob or does this module not support Roblox types?:confused:

Code used to test. Was trying to learn the
local repr = require(3148021300)

local region = Region3.new(Vector3.new(-8, -8, -8), Vector3.new(8, 8, 8))
print(repr(game.Workspace.Terrain:ReadVoxels(region, 4), {pretty = true, robloxFullName = false, robloxProperFullName = true, robloxClassName = true}))

Here’s a module made by Partixel that works with roblox types, cyclic tables and metatables

1 Like

I’ll add support for more Roblox types today.

Update: repr now supports all Roblox data types. It presents each data type using its closest constructor. Update the plugin.

Thank you for this! As a guy who HATES string manipulation, this is a major blessing from above.


Quick question: Have you attempted to make an OOP version of this? For example, it would be amazing to do:

print(myTable:repr(options));

Not forcing you, just a question to see if this would be compatible with OOP in the first place. (I have some time later so I may try to implement it myself.)

1 Like

Implementing this would be trivial:

t.repr = repr
t:repr(options)

You could use your own constructor to construct tables for consistent access to the function:

local function newTable(initialTable)
    initialTable.repr = repr
    return initialTable
end

local t = newTable({key = "value", otherKey = "otherValue"})
print(t:repr()) --> {key = "key", otherKey = "otherValue"}
2 Likes

Yep, that’s exactly what I came up with. Oh well. It was worth a shot. Thank you though.

Although the t.repr = repr isn’t actually super bad thinking about it now… One line of code for OOP use isn’t bad (considering all the other junk we have to use to get classes and such).

Edit: And Discourse decide to break my spoiler. I guess code and blur don’t go well together…

I’ve considered it. This is just the down-and-dirty “I want something printed, now” version. I’d like to make something like this possible:

local repr = require(...):default{pretty=true,semicolons=true}
local someTable = {...}
print(repr(someTable))

And to answer your question, yes! - it would be pretty easy to add :repr() methods to objects, given how colon-syntax works.

1 Like

This would’ve come in handy back when GetBundleDetailsAsync had no documentation (it was such a chore for me to manually print the returns) and I’m sure it’ll have great future usage for debugging. Cheers.

One thing to mention: the article linked in the thread is actually to the page for GetBundleDetailsSync, not GetBundleDetailsAsync. May want to correct that (and maybe specify what the former is for, since that has no documentation).

1 Like

I’ve updated the plugin to also expose _G.print_repr, which sends the result to print before returning it. So you can do even shorter checks in the command bar:

_G.print_repr{holy_cow = "it's a new update"} --> {holy_cow = "it's a new update"}

Update it from within Studio’s Plugins tab, under Manage Plugins:

2 Likes

This is pretty nice. I had my own function for this, but often ended up getting incorrect indents for whatever reason. Definitely going to use this.

Nice job, I was messing around with Players and Tables the other day and I kicked myself for the table printing something useless to me! Thanks.

Amazing module! Just noticed a small typo on line 278 where you forgot to include a closing ‘)’ in the string when printing out Vector2 values. image

4 Likes

Are you still maintaining this Module? @Ozzypig

The function get’s stuck when I pass an Indirect Cyclic Table.

I would like to provide more Detail and a working Repro, in private

2 Likes