9

How can I load a file of lua tables and variables without polluting the global environment? Since doing a loadfile and running it just loads everything in the global space and may overwrite something else which I don't want.

tshepang
  • 12,111
  • 21
  • 91
  • 136
Milind
  • 415
  • 8
  • 24
  • Are you using lua 5.2 or 5.1? – kikito Mar 02 '12 at 21:45
  • 1
    Found it: `x = loadfile("myfile.lua")` `setfenv(x,env)` `x() -- all global accesses would go to env rather then _G` – Milind Mar 02 '12 at 21:55
  • Exactly. You seem to be in 5.1 . You should answer yourself and mark your answer as correct, so it doesn't appear "unanswered", but you don't have the rep for that. :/ – kikito Mar 02 '12 at 22:02

3 Answers3

13

In Lua 5.1 and without much error handling you could do this:

-- load and run a script in the provided environment
-- returns the modified environment table
function run(scriptfile)
    local env = setmetatable({}, {__index=_G})
    assert(pcall(setfenv(assert(loadfile(scriptfile)), env)))
    setmetatable(env, nil)
    return env
end

The first line creates an empty environment table that can see all existing globals, but which cannot trivially change them since they are visible only by proxy through the __index metamethod. Any globals the script creates would be stored in env, which is returned. This will work well for simple scripts that just set a bunch of configuration parameters, and which might need to call simple safe functions to set them based on conditions at run time.

Note that making the globals visible to the script is a convenience. Although the globals cannot be modified from the script in the obvious way, _G is a global variable that contains a reference to the global environment (containing _G._G, _G._G._G, etc...) and _G can be modified from the script which could lead to further issues.

So rather than using _G for the index, it would be much better to construct a table that contains only functions known to be safe and known to be needed by your script's author.

A complete solution would be to run the script in a sandbox, and possibly further protected to prevent accidental (or deliberate) denial of service or worse. Sandboxes are covered in more detail at the Lua User's Wiki. The topic is deeper than it seems at first glance, but as long as your users are trusted to be non-malicious then practical solutions are straightforward.

Lua 5.2 changes things a little bit by eliminating setfenv() in favor of a new parameter to load(). Details are also in the wiki page.

RBerteig
  • 41,948
  • 7
  • 88
  • 128
  • 2
    One way to work around modifying `_G` is to set `_G` to something else in the environment: `local env = setmetatable({_G=false}, {__index=_G})` – Michal Kottman Mar 03 '12 at 09:51
  • Actually, I realized that it is really easy to break in the script: `_G=nil; _G.print=nil`. So something like this would be needed: `local env = setmetatable({}, {__index=function(t,k) if k=='_G' then return nil else return _G[k] end})` – Michal Kottman Mar 03 '12 at 10:00
  • One key idea expressed in the wiki page that I didn't bring up in my answer is to use a whitelist of functions that are carefully selected as safe and needed *rather* than allowing _G or any other globally known module table to leak into the script's environment. Its far safer to let `string.find` into the environment than `string` itself, for instance. That said, the answer as I gave it is quite practical for config files of small non-secure utilities. – RBerteig Mar 05 '12 at 22:17
  • Missing a close paren in line 2? – RAL May 03 '13 at 18:53
  • @RobertLamb, And here I swore I copied and pasted from working code.... fixed, thanks. – RBerteig May 03 '13 at 19:03
  • I'm having an issue when I try this solution. It works fine when run at first, but let's say `env` is returned with a function `env.greet()` that prints `hi` when run. Including the `setmetatable(env, nil)` line means that when `env.greet()` is run, it has no access to the print function. `attempt to index global 'print' (a nil value)`. Any workarounds? – Merlin Katz Aug 11 '22 at 03:12
3

Here is a dofile() version of RBerteig's answer where you supply the environment and the result, if any, is returned (I tried to do this as a comment, but couldn't figure out to format it):

local function DofileIntoEnv(filename, env)
    setmetatable ( env, { __index = _G } )
    local status, result = assert(pcall(setfenv(assert(loadfile(filename)), env)))
    setmetatable(env, nil)
    return result
end

I wanted to be able to load multiple files into the same environment, and some of these files had a 'return something' in them. Thanks RBerteig, your answer was helpful and instructive!

RAL
  • 917
  • 8
  • 19
0

In Lua > 5.2

function run_test_script(scriptfile)
    local env = setmetatable({}, {__index=_G})
    assert(pcall(loadfile(scriptfile,"run_test_script",env)))
    setmetatable(env, nil)
    return env
end