Table to String converter

Hello all.
I’ve recently been working on my debug console to patch & find bugs easier, and a huge function I needed was a table to string converter. Basically, this takes in an input table and outputs a string of the table and it’s contents.

Capability

  • Extremely detailed data about cyclic tables - marvel at this beauty! This module even provides the path of the cyclic table :slight_smile:
    image

  • More detailed information of threads - the threads current state is also shown.

  • Printing out userdata shows newproxy(bool), where bool is true / false depending on whether the proxy has a metatable.

  • Printing out unknown functions now provides MUCH more information - it now compares the function with pretty much every single C function (such as print, warn, etc) in a lookup table, and if it doesn’t find the function’s name it simply states whether the function is a C or Lua function or not, along with it’s address.

  • There are no indices shown by default so long the array maintains it’s order - i.e it is shown as{1, 2, 3} rather than {[1] = 1, [2] = 2, [3] = 3}.

  • Printing out Instances makes sure the path is valid, i.e methods such as :GetFullName would show Workspace.Terrain.and, whereas this module shows Workspace.Terrain["and"], which is valid syntax.

  • String indices change from using square brackets and not depending on whether or not the index has valid syntax.

  • Support of ALL Roblox types (CFrame, Vector3, …), with no need to manually update

  • Properly escapes special characters such as \n, \r, and \0.

  • Supports tables as keys - no issues there, even spacing-wise!

You can take a look at an example input and output down below.

Documentation

The one and only argument for this function is just the table you wish to convert into a string.
All the other arguments are used internally; please don’t define them unless you know what you’re doing.
Sample input & output:

input
{
	game.Lighting.Atmosphere,
	workspace.Terrain['and'],
	CFrame.new(),
	UDim2.new(),
	Color3.new(),
	['and'] = 'keyword test',
	['continue'] = 'keyword test 2',
	'\n\r\0\v',
	1233253,
	newproxy(),
	print,
	warn,
	CFrame.new,
	os.time,
	bit32.band,
	getmetatable,
	game.ItemChanged.Connect,
	coroutine.running(),
	[{
		1
	}] = 'hi',
	cyclicTest
}

image

Source

The source can be found here: paste.sh · encrypted pastebin

Alternatively, here's the source
-- Created by Pyseph#1015

local SpecialCharacters = {['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t', ['\v'] = '\\v', ['\0'] = '\\0'}
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, ['continue'] = true}
local Functions = {[DockWidgetPluginGuiInfo.new] = "DockWidgetPluginGuiInfo.new"; [warn] = "warn"; [CFrame.fromMatrix] = "CFrame.fromMatrix"; [CFrame.fromAxisAngle] = "CFrame.fromAxisAngle"; [CFrame.fromOrientation] = "CFrame.fromOrientation"; [CFrame.fromEulerAnglesXYZ] = "CFrame.fromEulerAnglesXYZ"; [CFrame.Angles] = "CFrame.Angles"; [CFrame.fromEulerAnglesYXZ] = "CFrame.fromEulerAnglesYXZ"; [CFrame.new] = "CFrame.new"; [gcinfo] = "gcinfo"; [os.clock] = "os.clock"; [os.difftime] = "os.difftime"; [os.time] = "os.time"; [os.date] = "os.date"; [tick] = "tick"; [bit32.band] = "bit32.band"; [bit32.extract] = "bit32.extract"; [bit32.bor] = "bit32.bor"; [bit32.bnot] = "bit32.bnot"; [bit32.arshift] = "bit32.arshift"; [bit32.rshift] = "bit32.rshift"; [bit32.rrotate] = "bit32.rrotate"; [bit32.replace] = "bit32.replace"; [bit32.lshift] = "bit32.lshift"; [bit32.lrotate] = "bit32.lrotate"; [bit32.btest] = "bit32.btest"; [bit32.bxor] = "bit32.bxor"; [pairs] = "pairs"; [NumberSequence.new] = "NumberSequence.new"; [assert] = "assert"; [tonumber] = "tonumber"; [Color3.fromHSV] = "Color3.fromHSV"; [Color3.toHSV] = "Color3.toHSV"; [Color3.fromRGB] = "Color3.fromRGB"; [Color3.new] = "Color3.new"; [Delay] = "Delay"; [Stats] = "Stats"; [UserSettings] = "UserSettings"; [coroutine.resume] = "coroutine.resume"; [coroutine.yield] = "coroutine.yield"; [coroutine.running] = "coroutine.running"; [coroutine.status] = "coroutine.status"; [coroutine.wrap] = "coroutine.wrap"; [coroutine.create] = "coroutine.create"; [coroutine.isyieldable] = "coroutine.isyieldable"; [NumberRange.new] = "NumberRange.new"; [PhysicalProperties.new] = "PhysicalProperties.new"; [PluginManager] = "PluginManager"; [Ray.new] = "Ray.new"; [NumberSequenceKeypoint.new] = "NumberSequenceKeypoint.new"; [Version] = "Version"; [Vector2.new] = "Vector2.new"; [Instance.new] = "Instance.new"; [delay] = "delay"; [spawn] = "spawn"; [unpack] = "unpack"; [string.split] = "string.split"; [string.match] = "string.match"; [string.gmatch] = "string.gmatch"; [string.upper] = "string.upper"; [string.gsub] = "string.gsub"; [string.format] = "string.format"; [string.lower] = "string.lower"; [string.sub] = "string.sub"; [string.pack] = "string.pack"; [string.rep] = "string.rep"; [string.char] = "string.char"; [string.packsize] = "string.packsize"; [string.reverse] = "string.reverse"; [string.byte] = "string.byte"; [string.unpack] = "string.unpack"; [string.len] = "string.len"; [string.find] = "string.find"; [CellId.new] = "CellId.new"; [ypcall] = "ypcall"; [version] = "version"; [print] = "print"; [stats] = "stats"; [printidentity] = "printidentity"; [settings] = "settings"; [UDim2.fromOffset] = "UDim2.fromOffset"; [UDim2.fromScale] = "UDim2.fromScale"; [UDim2.new] = "UDim2.new"; [table.pack] = "table.pack"; [table.move] = "table.move"; [table.insert] = "table.insert"; [table.getn] = "table.getn"; [table.foreachi] = "table.foreachi"; [table.maxn] = "table.maxn"; [table.foreach] = "table.foreach"; [table.concat] = "table.concat"; [table.unpack] = "table.unpack"; [table.find] = "table.find"; [table.create] = "table.create"; [table.sort] = "table.sort"; [table.remove] = "table.remove"; [TweenInfo.new] = "TweenInfo.new"; [loadstring] = "loadstring"; [require] = "require"; [Vector3.FromNormalId] = "Vector3.FromNormalId"; [Vector3.FromAxis] = "Vector3.FromAxis"; [Vector3.fromAxis] = "Vector3.fromAxis"; [Vector3.fromNormalId] = "Vector3.fromNormalId"; [Vector3.new] = "Vector3.new"; [Vector3int16.new] = "Vector3int16.new"; [setmetatable] = "setmetatable"; [next] = "next"; [Wait] = "Wait"; [wait] = "wait"; [ipairs] = "ipairs"; [elapsedTime] = "elapsedTime"; [time] = "time"; [rawequal] = "rawequal"; [Vector2int16.new] = "Vector2int16.new"; [collectgarbage] = "collectgarbage"; [newproxy] = "newproxy"; [Spawn] = "Spawn"; [PluginDrag.new] = "PluginDrag.new"; [Region3.new] = "Region3.new"; [utf8.offset] = "utf8.offset"; [utf8.codepoint] = "utf8.codepoint"; [utf8.nfdnormalize] = "utf8.nfdnormalize"; [utf8.char] = "utf8.char"; [utf8.codes] = "utf8.codes"; [utf8.len] = "utf8.len"; [utf8.graphemes] = "utf8.graphemes"; [utf8.nfcnormalize] = "utf8.nfcnormalize"; [xpcall] = "xpcall"; [tostring] = "tostring"; [rawset] = "rawset"; [PathWaypoint.new] = "PathWaypoint.new"; [DateTime.fromUnixTimestamp] = "DateTime.fromUnixTimestamp"; [DateTime.now] = "DateTime.now"; [DateTime.fromIsoDate] = "DateTime.fromIsoDate"; [DateTime.fromUnixTimestampMillis] = "DateTime.fromUnixTimestampMillis"; [DateTime.fromLocalTime] = "DateTime.fromLocalTime"; [DateTime.fromUniversalTime] = "DateTime.fromUniversalTime"; [Random.new] = "Random.new"; [typeof] = "typeof"; [RaycastParams.new] = "RaycastParams.new"; [math.log] = "math.log"; [math.ldexp] = "math.ldexp"; [math.rad] = "math.rad"; [math.cosh] = "math.cosh"; [math.random] = "math.random"; [math.frexp] = "math.frexp"; [math.tanh] = "math.tanh"; [math.floor] = "math.floor"; [math.max] = "math.max"; [math.sqrt] = "math.sqrt"; [math.modf] = "math.modf"; [math.pow] = "math.pow"; [math.atan] = "math.atan"; [math.tan] = "math.tan"; [math.cos] = "math.cos"; [math.sign] = "math.sign"; [math.clamp] = "math.clamp"; [math.log10] = "math.log10"; [math.noise] = "math.noise"; [math.acos] = "math.acos"; [math.abs] = "math.abs"; [math.sinh] = "math.sinh"; [math.asin] = "math.asin"; [math.min] = "math.min"; [math.deg] = "math.deg"; [math.fmod] = "math.fmod"; [math.randomseed] = "math.randomseed"; [math.atan2] = "math.atan2"; [math.ceil] = "math.ceil"; [math.sin] = "math.sin"; [math.exp] = "math.exp"; [getfenv] = "getfenv"; [pcall] = "pcall"; [ColorSequenceKeypoint.new] = "ColorSequenceKeypoint.new"; [ColorSequence.new] = "ColorSequence.new"; [type] = "type"; [Region3int16.new] = "Region3int16.new"; [ElapsedTime] = "ElapsedTime"; [select] = "select"; [getmetatable] = "getmetatable"; [rawget] = "rawget"; [Faces.new] = "Faces.new"; [Rect.new] = "Rect.new"; [BrickColor.Blue] = "BrickColor.Blue"; [BrickColor.White] = "BrickColor.White"; [BrickColor.Yellow] = "BrickColor.Yellow"; [BrickColor.Red] = "BrickColor.Red"; [BrickColor.Gray] = "BrickColor.Gray"; [BrickColor.palette] = "BrickColor.palette"; [BrickColor.New] = "BrickColor.New"; [BrickColor.Black] = "BrickColor.Black"; [BrickColor.Green] = "BrickColor.Green"; [BrickColor.Random] = "BrickColor.Random"; [BrickColor.DarkGray] = "BrickColor.DarkGray"; [BrickColor.random] = "BrickColor.random"; [BrickColor.new] = "BrickColor.new"; [setfenv] = "setfenv"; [UDim.new] = "UDim.new"; [Axes.new] = "Axes.new"; [error] = "error"; [debug.traceback] = "debug.traceback"; [debug.profileend] = "debug.profileend"; [debug.profilebegin] = "debug.profilebegin"}

function GetHierarchy(Object)
	local Hierarchy = {}

	local ChainLength = 1
	local Parent = Object
	
	while Parent do
		Parent = Parent.Parent
		ChainLength = ChainLength + 1
	end

	Parent = Object
	local Num = 0
	while Parent do
		Num = Num + 1

		local ObjName = string.gsub(Parent.Name, '[%c%z]', SpecialCharacters)
		ObjName = Parent == game and 'game' or ObjName

		if Keywords[ObjName] or not string.match(ObjName, '^[_%a][_%w]*$') then
			ObjName = '["' .. ObjName .. '"]'
		elseif Num ~= ChainLength - 1 then
			ObjName = '.' .. ObjName
		end

		Hierarchy[ChainLength - Num] = ObjName
		Parent = Parent.Parent
	end

	return table.concat(Hierarchy)
end
local function SerializeType(Value, Class)
	if Class == 'string' then
		-- Not using %q as it messes up the special characters fix
		return string.format('"%s"', string.gsub(Value, '[%c%z]', SpecialCharacters))
	elseif Class == 'Instance' then
		return GetHierarchy(Value)
	elseif type(Value) ~= Class then -- CFrame, Vector3, UDim2, ...
		return Class .. '.new(' .. tostring(Value) .. ')'
	elseif Class == 'function' then
		return Functions[Value] or '\'[Unknown ' .. (pcall(setfenv, Value, getfenv(Value)) and 'Lua' or 'C')  .. ' ' .. tostring(Value) .. ']\''
	elseif Class == 'userdata' then
		return 'newproxy(' .. tostring(not not getmetatable(Value)) .. ')'
	elseif Class == 'thread' then
		return '\'' .. tostring(Value) ..  ', status: ' .. coroutine.status(Value) .. '\''
	else -- thread, number, boolean, nil, ...
		return tostring(Value)
	end
end
local function TableToString(Table, IgnoredTables, DepthData, Path)
	IgnoredTables = IgnoredTables or {}
	local CyclicData = IgnoredTables[Table]
	if CyclicData then
		return ((CyclicData[1] == DepthData[1] - 1 and '\'[Cyclic Parent ' or '\'[Cyclic ') .. tostring(Table) .. ', path: ' .. CyclicData[2] .. ']\'')
	end

	Path = Path or 'ROOT'
	DepthData = DepthData or {0, Path}
	local Depth = DepthData[1] + 1
	DepthData[1] = Depth
	DepthData[2] = Path

	IgnoredTables[Table] = DepthData
	local Tab = string.rep('    ', Depth)
	local TrailingTab = string.rep('    ', Depth - 1)
	local Result = '{'

	local LineTab = '\n' .. Tab
	local HasOrder = true
	local Index = 1

	local IsEmpty = true
	for Key, Value in next, Table do
		IsEmpty = false
		if Index ~= Key then
			HasOrder = false
		else
			Index = Index + 1
		end

		local KeyClass, ValueClass = typeof(Key), typeof(Value)
		local HasBrackets = false
		if KeyClass == 'string' then
			Key = string.gsub(Key, '[%c%z]', SpecialCharacters)
			if Keywords[Key] or not string.match(Key, '^[_%a][_%w]*$') then
				HasBrackets = true
				Key = string.format('["%s"]', Key)
			end
		else
			HasBrackets = true
			Key = '[' .. (KeyClass == 'table' and string.gsub(TableToString(Key, IgnoredTables, {Depth + 1, Path}), '^%s*(.-)%s*$', '%1') or SerializeType(Key, KeyClass)) .. ']'
		end

		Value = ValueClass == 'table' and TableToString(Value, IgnoredTables, {Depth + 1, Path}, Path .. (HasBrackets and '' or '.') .. Key) or SerializeType(Value, ValueClass)
		Result = Result .. LineTab .. (HasOrder and Value or Key .. ' = ' .. Value) .. ','
	end

	return IsEmpty and Result .. '}' or string.sub(Result,  1, -2) .. '\n' .. TrailingTab .. '}'
end
51 Likes

I was about to code this today actually. Will definitely come in handy for compressing data.

Much appreciated, credit will be added in source.

5 Likes

Updated; this now gets the names of global functions (math.random, tick, …).
I do this by just storing a dictionary which has the function as the key, and the function’s name as the value.
Example:

local Functions = {
    [print] = 'print'
}
print(Functions[print]) --> print

If it can’t find the function in the globals list, it simply says [Unknown function, C Closure: true/false].

Updated; cyclic table detection now tells you whether the cyclic table is a direct reference to it’s parent table or not.
What I mean:

local MyTable = {}
local SomeTable = {}
MyTable[1] = SomeTable
SomeTable.Value = MyTable -- 'SomeTable' now directly resides in MyTable, which is it's parent.

I’ve been working with linked lists lately, and this gave me a hand with debugging.

Not to be aimlessly critical of this but what does your module have to offer that repr doesn’t?

I’m unsure what ‘repr’ is. Could you link me the resource?

At a first glance, the module seems to be a lot slower than mine (since it has to manually check for every type due to the module supporting löve, factorio etc.); this module is meant for debugging in Roblox, and hence doesn’t have any of that functionality, making this much more lightweight and faster than Repr.

Is there a way to deserialize it? as in turn the string back into a table?

You can convert most of the values in the table, but if you store functions and such in that table then it would be impossible to deserialize or even serialize (unless you have access to bytecode) them. This table serializer alleviates this to some extent though, by being able to serialize some C functions such as print, warn etc.

All I have to say is… THANK YOU. Literally a life saver.

Update [1.0.1]

  • There is now much more information shown regarding cyclic tables - marvel at this beauty!
    image
    Code used:
local cyclicTest = {
	a = {}
}
cyclicTest.a.b = cyclicTest.a

If the table which the index references is the parent table which the index resides in, it will say Cyclic Parent Table rather than just Cyclic Table, as seen in that image :slight_smile: .

  • Added more detailed information to threads - the threads current state is also printed.
  • Printing out userdata has changed - now it simply is newproxy(bool), where bool is true / false depending on whether the proxy has a metatable.
  • Printing out unknown functions now provides more information - it now also says whether the function is a C or Lua function or not, along with it’s address.
  • There is now no indices shown by default so long the array maintains it’s order - i.e it’s now {1, 2, 3} rather than {[1] = 1, [2] = 2, [3] = 3}.
  • Printing out Instances now makes sure the path is valid, i.e previously it would be Workspace.Terrain.and, where now it is Workspace.Terrain["and"]. It now also takes into account for game at the start.
  • String indices now change from using square brackets and not, depending on whether the index is a keyword, contains a number as its first character, contains whitespace and so on.
  • Cleaned up source: for instance, s:format(...) is now string.format(s, ...), as this is now the faster method due to LuaU’s optimizations.

Oh hi sir lol.
Apparently we are looking for the same things.

Hello, yah this helped me out when I was making a plugin.

Late, but here is a way to automatically get the global functions instead of hard-coding them and having “Unknown C function: …” be returned.

local Functions = {};

-- get global functions automatically
for i,v in ipairs({getgenv(),getrenv()})do
    for fol,v1 in next,v do
        if(type(fol)~="string")then
            continue;
        elseif(type(v1)=="function"and not table.find(Functions,v1))then
            Functions[v1] = fol;
        elseif(type(v1)=="table")then
            for i1,v2 in next,v1 do
                if(type(v2)=="function"and not table.find(Functions,v2))then
                    Functions[v2] = fol..'.'..i1;
                end;
            end;
        end;
    end;
end;

getgenv and getrenv are exploit functions.

is it different than tostring({workspace})?

also great module :slight_smile:

And how do you convert from string to table?