diff --git a/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/require.lua b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/require.lua new file mode 100644 index 000000000..a90e189b8 --- /dev/null +++ b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/require.lua @@ -0,0 +1,121 @@ +--- This provides a pure Lua implementation of the builtin @{require} function +-- and @{package} library. +-- +-- Generally you do not need to use this module - it is injected into the +-- every program's environment. However, it may be useful when building a +-- custom shell or when running programs yourself. +-- +-- @module cc.require +-- @usage Construct the package and require function, and insert them into a +-- custom environment. +-- +-- local env = setmetatable({}, { __index = _ENV }) +-- local r = require "cc.require" +-- env.require, env.package = r.make(env, "/") + +local expect = require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua") +local expect = expect.expect + +local function preload(package) + return function(name) + if package.preload[name] then + return package.preload[name] + else + return nil, "no field package.preload['" .. name .. "']" + end + end +end + +local function from_file(package, env, dir) + return function(name) + local fname = string.gsub(name, "%.", "/") + local sError = "" + for pattern in string.gmatch(package.path, "[^;]+") do + local sPath = string.gsub(pattern, "%?", fname) + if sPath:sub(1, 1) ~= "/" then + sPath = fs.combine(dir, sPath) + end + if fs.exists(sPath) and not fs.isDir(sPath) then + local fnFile, sError = loadfile(sPath, nil, env) + if fnFile then + return fnFile, sPath + else + return nil, sError + end + else + if #sError > 0 then + sError = sError .. "\n " + end + sError = sError .. "no file '" .. sPath .. "'" + end + end + return nil, sError + end +end + +local function make_require(package) + local sentinel = {} + return function(name) + expect(1, name, "string") + + if package.loaded[name] == sentinel then + error("loop or previous error loading module '" .. name .. "'", 0) + end + + if package.loaded[name] then + return package.loaded[name] + end + + local sError = "module '" .. name .. "' not found:" + for _, searcher in ipairs(package.loaders) do + local loader = table.pack(searcher(name)) + if loader[1] then + package.loaded[name] = sentinel + local result = loader[1](name, table.unpack(loader, 2, loader.n)) + if result == nil then result = true end + + package.loaded[name] = result + return result + else + sError = sError .. "\n " .. loader[2] + end + end + error(sError, 2) + end +end + +--- Build an implementation of Lua's @{package} library, and a @{require} +-- function to load modules within it. +-- +-- @tparam table env The environment to load packages into. +-- @tparam string dir The directory that relative packages are loaded from. +-- @treturn function The new @{require} function. +-- @treturn table The new @{package} library. +local function make_package(env, dir) + expect(1, env, "table") + expect(2, dir, "string") + + local package = {} + package.loaded = { + _G = _G, + bit32 = bit32, + coroutine = coroutine, + math = math, + package = package, + string = string, + table = table, + } + package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua" + if turtle then + package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua" + elseif commands then + package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua" + end + package.config = "/\n;\n?\n!\n-" + package.preload = {} + package.loaders = { preload(package), from_file(package, env, dir) } + + return make_require(package), package +end + +return { make = make_package } diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua b/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua index 843e28e71..0c6b5868b 100644 --- a/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua +++ b/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua @@ -11,6 +11,7 @@ -- @module[module] shell local expect = dofile("rom/modules/main/cc/expect.lua").expect +local make_package = dofile("rom/modules/main/cc/require.lua").make local multishell = multishell local parentShell = shell @@ -28,94 +29,10 @@ local tCompletionInfo = parentShell and parentShell.getCompletionInfo() or {} local tProgramStack = {} local shell = {} --- @export -local function createShellEnv(sDir) - local tEnv = {} - tEnv.shell = shell - tEnv.multishell = multishell - - local package = {} - package.loaded = { - _G = _G, - bit32 = bit32, - coroutine = coroutine, - math = math, - package = package, - string = string, - table = table, - } - package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua" - if turtle then - package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua" - elseif commands then - package.path = package.path .. ";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua" - end - package.config = "/\n;\n?\n!\n-" - package.preload = {} - package.loaders = { - function(name) - if package.preload[name] then - return package.preload[name] - else - return nil, "no field package.preload['" .. name .. "']" - end - end, - function(name) - local fname = string.gsub(name, "%.", "/") - local sError = "" - for pattern in string.gmatch(package.path, "[^;]+") do - local sPath = string.gsub(pattern, "%?", fname) - if sPath:sub(1, 1) ~= "/" then - sPath = fs.combine(sDir, sPath) - end - if fs.exists(sPath) and not fs.isDir(sPath) then - local fnFile, sError = loadfile(sPath, nil, tEnv) - if fnFile then - return fnFile, sPath - else - return nil, sError - end - else - if #sError > 0 then - sError = sError .. "\n " - end - sError = sError .. "no file '" .. sPath .. "'" - end - end - return nil, sError - end, - } - - local sentinel = {} - local function require(name) - expect(1, name, "string") - if package.loaded[name] == sentinel then - error("loop or previous error loading module '" .. name .. "'", 0) - end - if package.loaded[name] then - return package.loaded[name] - end - - local sError = "module '" .. name .. "' not found:" - for _, searcher in ipairs(package.loaders) do - local loader = table.pack(searcher(name)) - if loader[1] then - package.loaded[name] = sentinel - local result = loader[1](name, table.unpack(loader, 2, loader.n)) - if result == nil then result = true end - - package.loaded[name] = result - return result - else - sError = sError .. "\n " .. loader[2] - end - end - error(sError, 2) - end - - tEnv.package = package - tEnv.require = require - - return tEnv +local function createShellEnv(dir) + local env = { shell = shell, multishell = multishell } + env.require, env.package = make_package(env, dir) + return env end -- Colours diff --git a/src/test/resources/test-rom/spec/modules/cc/shell/require_spec.lua b/src/test/resources/test-rom/spec/modules/cc/shell/require_spec.lua new file mode 100644 index 000000000..721476f9d --- /dev/null +++ b/src/test/resources/test-rom/spec/modules/cc/shell/require_spec.lua @@ -0,0 +1,79 @@ +describe("cc.require", function() + local r = require "cc.require" + local function mk() + local env = setmetatable({}, { __index = _ENV }) + env.require, env.package = r.make({}, "/test-files/modules") + return env.require, env.package + end + + local function setup(path, contents) + fs.delete("/test-files/modules") + io.open(path, "w"):write(contents):close() + end + + describe("require", function() + it("errors on recursive modules", function() + local require, package = mk() + package.preload.pkg = function() require "pkg" end + expect.error(require, "pkg"):eq("loop or previous error loading module 'pkg'") + end) + + it("supplies the current module name", function() + local require, package = mk() + package.preload.pkg = table.pack + expect(require("pkg")):same { n = 1, "pkg" } + end) + + it("returns true instead of nil", function() + local require, package = mk() + package.preload.pkg = function() return nil end + expect(require("pkg")):eq(true) + end) + + it("returns a constant value", function() + local require, package = mk() + package.preload.pkg = function() return {} end + expect(require("pkg")):eq(require("pkg")) + end) + + it("returns an error on not-found modules", function() + local require, package = mk() + package.path = "/?;/?.lua" + expect.error(require, "pkg"):eq( + "module 'pkg' not found:\n" .. + " no field package.preload['pkg']\n" .. + " no file '/pkg'\n" .. + " no file '/pkg.lua'") + end) + end) + + describe("the file loader", function() + local function get(path) + local require, package = mk() + if path then package.path = path end + return require + end + + it("works on absolute paths", function() + local require = get("/test-files/?.lua") + setup("test-files/some_module.lua", "return 123") + expect(require("some_module")):eq(123) + end) + + it("works on relative paths", function() + local require = get("?.lua") + setup("test-files/modules/some_module.lua", "return 123") + expect(require("some_module")):eq(123) + end) + + it("fails on syntax errors", function() + local require = get("?.lua") + setup("test-files/modules/some_module.lua", "1") + expect.error(require, "some_module"):str_match( + "^module 'some_module' not found:\n" .. + " no field package.preload%['some_module'%]\n" .. + " [^:]*some_module.lua:1: unexpected symbol near '1'$" + ) + end) + end) +end)