Scripting HelpersARCHIVE
A little piece of Roblox history. Preserved for everyone, one question at a time.Read about the archive →
✓ ANSWER ACCEPTED

How would I convert a string to an object reference?

For example, if I was given a string such as "game.Workspace.model", how would I convert that to an object reference? It likely could be done with loadstring, such as in the below example, but compiling code with loadstring is a very inefficient way to do things. Is there a better way to do this? Thank you.

local object
loadstring("object = game.Workspace.model")()

1 answer

✓ Accepted answer

Preface

loadstring = bad

Preface by saying: Using loadstring is essentially never a good idea because it is dangerous. (Allowing a user to enter text to find a part would also allow them to obliterate your place, or save it to somewhere public, etc)

object values = good

Instead of storing a string, just store the object itself. E.g., instead of using "workspace.Part" use just workspace.Part. Then you don't have to do the to string and from string at all.


Complexities

There can be some amount of complexity in this problem, from the following concerns:

Names aren't necessarily valid identifiers:

  • They can contain punctuation
  • They can contain spaces
  • They can start with non letters

More than the period is used to identify child names (can be because of the above):

  • You can use brackets combined with strings
  • Strings have 3 quote styles
  • Strings are expressions so technically computation could be in here, but hopefully you don't want that
  • Periods and brackets are valid characters in names

As far as I know, :GetFullName() ignores these complexities entirely. Thus if a part is named Part.Blah then its :GetFullName() will be game.Workspace.Part.Blah, etc. You have been warned.


Simple Solution

  • Assumes all names are valid identifiers and do not contain periods
  • Periods are the only identifiers for child

In this case, it's a fairly simple problem once we get a string split function. Here is one from the Lua-users Wiki:

function split(self, sep)
        local sep, fields = sep or ":", {};
        local pattern = string.format("([^%s]+)", sep);
        self:gsub(pattern, function(c) fields[#fields+1] = c end);
        return fields;
end

Our first function could look like this:

function getObject(path)
	local obj = {
		game=game,Game=game,
		Workspace=workspace,
		workspace=workspace,
		script=script
	};
	local names = split(path,".");

	for _,name in pairs(names) do
		obj = obj[name];
	end
	return obj;
end

Most of the time, this is enough. If we want to do better...


Super Serious Solution

I do a finite state parse and use this to do things like escaping and whatnot after.

Here is the full monster:

function parseStates(text)
	local out = {};
	text = text:sub(text:find("[a-zA-Z_]") ,-1);
	-- Set up states
	local IDENTIFIER = "a";
	local DOUBLE = [["]];
	local SINGLE = [[']];
	local BRACKETS = "{";
	local EBRACKET = ")";
	local BRACKETS1 = "(";
	local INDEX = "[";
	local state = IDENTIFIER;
	-- Set up sub states
	local sub = nil;

	local escaping = false;

	for i = 1,#text do
		out[i] = "?";
		local cs = state;
		local char = text:sub(i,i);
		local remainder = text:sub(i);
		if cs == IDENTIFIER then
			if char:gsub("[a-zA-Z0-9]","") == "" then
				-- Continuation of name
				out[i] = IDENTIFIER;
			else
				if char:gsub("%s","") == "" then
				-- Space: Still identifier (wait for [ or .)
					out[i] = " ";
				else
					if char == "." then
						out[i] = ".";
						state = IDENTIFIER;
					elseif char == "[" then
						state = INDEX;
						out[i] = "[";
					else
						-- Parse error
						return nil;
					end
				end
			end
		elseif cs == DOUBLE or cs == SINGLE then
			local escaped = escaping;
			escaping = false;
			out[i] = "x";
			if escaped then
				out[i] = "1";
			else
				local isnum = char:gsub("%d","") == "";
				if out[i-1] == "1" and isnum
					and text:sub(i-1,i-1):gsub("%d","") == "" then
					out[i] = "2";
				elseif out[i-1] == "2" and isnum then
					out[i] = "3";
				else
					if char == cs then
						out[i] = state;
						state = INDEX;
					elseif char == [[\]] then
						escaping = i;
						out[i] = [[\]]
					end
				end
			end
		elseif cs == EBRACKET then
			out[i] = "}";
			state = INDEX;
		elseif cs == BRACKETS then
			-- Double brackets.
			out[i] = "s";
			if remainder:sub(1,2) == "]]" then
				out[i] = "}";
				state = EBRACKET;
			end
		elseif cs == BRACKETS1 then
			state = BRACKETS;
			out[i] = "{";
		elseif cs == INDEX then
			escaping = false;
			-- *INSIDE* (after) a [
			if char:gsub("%s","") == "" then
				out[i] = " ";
			else
				if remainder:sub(1,2) == "[[" then
					state = BRACKETS1;
					out[i] = "{";
				elseif char == "'" or char == '"' then
					state = char;
					out[i] = state;
				elseif char == "]" then
					out[i] = "]";
					state = IDENTIFIER;
				else
					-- Parse error
					print(text);
					print((" "):rep(i-1) .. "^");
					return nil;
				end
			end
		end
	end
	return text, table.concat(out,"");
end

function escapes(str,states)
	-- Find numbers in states and combine
	-- them with their backslash (also in states) to make new characters
	local s = "";
	while #str > 0 do
		if states:sub(1,1) == "\\" then
			if states:sub(4,4) == "3" then
				-- "\123"
				local seq = str:sub(2,4);
				s = s .. string.char( tonumber( seq ) );
				states = states:sub(5);
				str = str:sub(5);
			elseif states:sub(3,3) == "2" then
				-- "\12"
				local seq = str:sub(2,3);
				s = s .. string.char( tonumber( seq ) );
				states = states:sub(4);
				str = str:sub(4);
			else
				-- "\1"
				local seq = str:sub(2,2);
				local n = tonumber(seq);
				if n then
					s = s .. string.char( n );
				else
					local e = {
						t="\t", n="\n", r="\r", v="\v", a="\a", b="\b", f="\f"
					};
					s = s .. (e[seq] or seq);
					states = states:sub(3);
					str = str:sub(3);
				end
			end
		else
			s = s .. str:sub(1,1);
			str = str:sub(2);
			states = states:sub(2);
		end
	end
	return s;
end

function parseStrings(text,states)
	function fits(mode,char)
		if mode == "a" then
			return char == "a";
		end
		if mode == "s" then
			return char == "s";
		end
		if mode == "x" or mode == "\\" then
			return char == "x" or char == "\\" or char == "1"
			or char == "2" or char == "3";
		end
	end
	-- Take adjacent As to turn into a literal string (just copy)
	-- Take adjacent Ss to turn into a literal string (just copy)
	-- Take Xs + 1s + 2s + 3s + \s to escape into a string.
	-- All others are separators
	local t = {};
	local s = "";
	local n = "";
	states = states .. " ";
	text = text .. " ";
	for i = 1, #states do
		if fits(n,states:sub(i,i)) then
			s = s .. text:sub(i,i);
		else
			if s ~= "" then
				if fits("a",n) or fits("s",n) or fits("x",n) then
					if fits("x",n) then
						s = escapes(s,states:sub(i-#s,i-1));
					end
					table.insert(t,s);
				end
			end
			s = text:sub(i,i);
			n = states:sub(i,i);
		end
	end
	return t;
end

function getNames(text)
	return parseStrings(parseStates(text));
end

function getObject(text,roots)
	local names = getNames(text);
	local obj = roots;
	for i = 1, #names do
		obj = obj[names[i]];
	end
	return obj;
end

Usage of this API:

Fully powered parser. Without anything unsafe like loadstring, it fully parses indexing using periods and brackets; it supports all three string types and correct character escaping.

Here is using it to extra a list of names (which is what getObject uses internally):

local text = [[game.Workspace .BlueTaslem["Left\n\tArm"]["Wow"].
Appl37.K ]];
print( text );
print("...");
print( 
		table.concat( getNames(text) , "\n" )
	 );
--[[
game.Workspace .BlueTaslem["Left\n\tArm"]["Wow"].
Appl37.K 
...
game
Workspace
BlueTaslem
Left
	Arm
Wow
Appl37
K
]]

And to actually get the object, we use getObject (and pass it all the names that you can start from)

local roots = {};
roots.game = game;
roots.workspace = workspace;
roots.Game = game;
roots.Workspace = Workspace;
roots.script = script;
-- You could also do this so you could
-- get to things like `"table.insert"`:
roots.table = table;

print(getObject("game.Workspace",roots));
--[[
Workspace
]]

Warning It's possible this will result in an error sometimes when you give it an invalid string.

Limitation

I forgot that Lua can use strings delimited by [=[ and ]=] (as in [=[ this is a string [=[ with nested marks ]=] ]=]

This does not support this (and cannot easily because [=[ can be nested).

The good news is I have never seen these used in practice as string literals.

This discussion is preserved as it was. New answers, comments, and votes are closed.