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")()
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)
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.
There can be some amount of complexity in this problem, from the following concerns:
Names aren't necessarily valid identifiers:
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.
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...
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
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.
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.