repr — function for printing Lua 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 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
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))

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.

Author: Ozzypig

Roblox developer and computer scientist. I love all things coding and game design!