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);
local IDENTIFIER = "a";
local DOUBLE = [["]];
local SINGLE = [[']];
local BRACKETS = "{";
local EBRACKET = ")";
local BRACKETS1 = "(";
local INDEX = "[";
local state = IDENTIFIER;
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
out[i] = IDENTIFIER;
else
if char:gsub("%s","") == "" then
out[i] = " ";
else
if char == "." then
out[i] = ".";
state = IDENTIFIER;
elseif char == "[" then
state = INDEX;
out[i] = "[";
else
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
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;
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
print(text);
print((" "):rep(i-1) .. "^");
return nil;
end
end
end
end
return text, table.concat(out,"");
end
function escapes(str,states)
local s = "";
while #str > 0 do
if states:sub(1,1) == "\\" then
if states:sub(4,4) == "3" then
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
local seq = str:sub(2,3);
s = s .. string.char( tonumber( seq ) );
states = states:sub(4);
str = str:sub(4);
else
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
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" )
);
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;
roots.table = table;
print(getObject("game.Workspace",roots));
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.