Scripting Helpers is winding down operations and is now read-only. More info→
Ad
Log in to vote
2

How would I convert a string to an object reference?

Asked by 10 years ago

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

Log in to vote
5
Answered by
BlueTaslem 18071 Moderation Voter Administrator Community Moderator Super Administrator
10 years ago

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.

Ad

Answer this question