mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-13 19:50:31 +00:00
Add support for shebangs (#1273)
This commit is contained in:
parent
f561572509
commit
f43b839056
@ -1,14 +1,39 @@
|
||||
--- The shell API provides access to CraftOS's command line interface.
|
||||
--
|
||||
-- It allows you to @{run|start programs}, @{setCompletionFunction|add
|
||||
-- completion for a program}, and much more.
|
||||
--
|
||||
-- @{shell} is not a "true" API. Instead, it is a standard program, which injects its
|
||||
-- API into the programs that it launches. This allows for multiple shells to
|
||||
-- run at the same time, but means that the API is not available in the global
|
||||
-- environment, and so is unavailable to other @{os.loadAPI|APIs}.
|
||||
--
|
||||
-- @module[module] shell
|
||||
--[[- The shell API provides access to CraftOS's command line interface.
|
||||
|
||||
It allows you to @{run|start programs}, @{setCompletionFunction|add completion
|
||||
for a program}, and much more.
|
||||
|
||||
@{shell} is not a "true" API. Instead, it is a standard program, which injects
|
||||
its API into the programs that it launches. This allows for multiple shells to
|
||||
run at the same time, but means that the API is not available in the global
|
||||
environment, and so is unavailable to other @{os.loadAPI|APIs}.
|
||||
|
||||
## Programs and the program path
|
||||
When you run a command with the shell, either from the prompt or
|
||||
@{shell.run|from Lua code}, the shell API performs several steps to work out
|
||||
which program to run:
|
||||
|
||||
1. Firstly, the shell attempts to resolve @{shell.aliases|aliases}. This allows
|
||||
us to use multiple names for a single command. For example, the `list`
|
||||
program has two aliases: `ls` and `dir`. When you write `ls /rom`, that's
|
||||
expanded to `list /rom`.
|
||||
|
||||
2. Next, the shell attempts to find where the program actually is. For this, it
|
||||
uses the @{shell.path|program path}. This is a colon separated list of
|
||||
directories, each of which is checked to see if it contains the program.
|
||||
|
||||
`list` or `list.lua` doesn't exist in `.` (the current directory), so she
|
||||
shell now looks in `/rom/programs`, where `list.lua` can be found!
|
||||
|
||||
3. Finally, the shell reads the file and checks if the file starts with a
|
||||
`#!`. This is a [hashbang][], which says that this file shouldn't be treated
|
||||
as Lua, but instead passed to _another_ program, the name of which should
|
||||
follow the `#!`.
|
||||
|
||||
[hashbang]: https://en.wikipedia.org/wiki/Shebang_(Unix)
|
||||
|
||||
@module[module] shell
|
||||
]]
|
||||
|
||||
local make_package = dofile("rom/modules/main/cc/require.lua").make
|
||||
|
||||
@ -54,6 +79,71 @@ else
|
||||
bgColour = colours.black
|
||||
end
|
||||
|
||||
local function tokenise(...)
|
||||
local sLine = table.concat({ ... }, " ")
|
||||
local tWords = {}
|
||||
local bQuoted = false
|
||||
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
|
||||
if bQuoted then
|
||||
table.insert(tWords, match)
|
||||
else
|
||||
for m in string.gmatch(match, "[^ \t]+") do
|
||||
table.insert(tWords, m)
|
||||
end
|
||||
end
|
||||
bQuoted = not bQuoted
|
||||
end
|
||||
return tWords
|
||||
end
|
||||
|
||||
-- Execute a program using os.run, unless a shebang is present.
|
||||
-- In that case, execute the program using the interpreter specified in the hashbang.
|
||||
-- This may occur recursively, up to the maximum number of times specified by remainingRecursion
|
||||
-- Returns the same type as os.run, which is a boolean indicating whether the program exited successfully.
|
||||
local function executeProgram(remainingRecursion, path, args)
|
||||
local file, err = fs.open(path, "r")
|
||||
if not file then
|
||||
printError(err)
|
||||
return false
|
||||
end
|
||||
|
||||
-- First check if the file begins with a #!
|
||||
local contents = file.readLine()
|
||||
file.close()
|
||||
|
||||
if contents and contents:sub(1, 2) == "#!" then
|
||||
remainingRecursion = remainingRecursion - 1
|
||||
if remainingRecursion == 0 then
|
||||
printError("Hashbang recursion depth limit reached when loading file: " .. path)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Load the specified hashbang program instead
|
||||
local hashbangArgs = tokenise(contents:sub(3))
|
||||
local originalHashbangPath = table.remove(hashbangArgs, 1)
|
||||
local resolvedHashbangProgram = shell.resolveProgram(originalHashbangPath)
|
||||
if not resolvedHashbangProgram then
|
||||
printError("Hashbang program not found: " .. originalHashbangPath)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Add the path and any arguments to the interpreter's arguments
|
||||
table.insert(hashbangArgs, path)
|
||||
for _, v in ipairs(args) do
|
||||
table.insert(hashbangArgs, v)
|
||||
end
|
||||
|
||||
hashbangArgs[0] = originalHashbangPath
|
||||
return executeProgram(remainingRecursion, resolvedHashbangProgram, hashbangArgs)
|
||||
end
|
||||
|
||||
local dir = fs.getDir(path)
|
||||
local env = createShellEnv(dir)
|
||||
env.arg = args
|
||||
|
||||
return os.run(env, path, table.unpack(args))
|
||||
end
|
||||
|
||||
--- Run a program with the supplied arguments.
|
||||
--
|
||||
-- Unlike @{shell.run}, each argument is passed to the program verbatim. While
|
||||
@ -84,10 +174,7 @@ function shell.execute(command, ...)
|
||||
multishell.setTitle(multishell.getCurrent(), sTitle)
|
||||
end
|
||||
|
||||
local sDir = fs.getDir(sPath)
|
||||
local env = createShellEnv(sDir)
|
||||
env.arg = { [0] = command, ... }
|
||||
local result = os.run(env, sPath, ...)
|
||||
local result = executeProgram(100, sPath, { [0] = command, ... })
|
||||
|
||||
tProgramStack[#tProgramStack] = nil
|
||||
if multishell then
|
||||
@ -108,23 +195,6 @@ function shell.execute(command, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function tokenise(...)
|
||||
local sLine = table.concat({ ... }, " ")
|
||||
local tWords = {}
|
||||
local bQuoted = false
|
||||
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
|
||||
if bQuoted then
|
||||
table.insert(tWords, match)
|
||||
else
|
||||
for m in string.gmatch(match, "[^ \t]+") do
|
||||
table.insert(tWords, m)
|
||||
end
|
||||
end
|
||||
bQuoted = not bQuoted
|
||||
end
|
||||
return tWords
|
||||
end
|
||||
|
||||
-- Install shell API
|
||||
|
||||
--- Run a program with the supplied arguments.
|
||||
@ -541,8 +611,8 @@ end
|
||||
--- Get the current aliases for this shell.
|
||||
--
|
||||
-- Aliases are used to allow multiple commands to refer to a single program. For
|
||||
-- instance, the `list` program is aliased `dir` or `ls`. Running `ls`, `dir` or
|
||||
-- `list` in the shell will all run the `list` program.
|
||||
-- instance, the `list` program is aliased to `dir` or `ls`. Running `ls`, `dir`
|
||||
-- or `list` in the shell will all run the `list` program.
|
||||
--
|
||||
-- @treturn { [string] = string } A table, where the keys are the names of
|
||||
-- aliases, and the values are the path to the program.
|
||||
|
@ -17,6 +17,67 @@ describe("The shell", function()
|
||||
|
||||
expect(args):same { [0] = "/test-rom/data/dump-args", "arg1", "arg 2" }
|
||||
end)
|
||||
|
||||
local function make_hashbang_file(target, filename)
|
||||
local tmp = fs.open(filename or "test-files/out.lua", "w")
|
||||
tmp.write("#!" .. target)
|
||||
tmp.close()
|
||||
end
|
||||
|
||||
it("supports hashbangs", function()
|
||||
make_hashbang_file("/test-rom/data/dump-args")
|
||||
shell.execute("test-files/out.lua", "arg1", "arg2")
|
||||
|
||||
local args = _G.__arg
|
||||
_G.__arg = nil
|
||||
|
||||
expect(args):same {
|
||||
[0] = "/test-rom/data/dump-args",
|
||||
"test-files/out.lua",
|
||||
"arg1",
|
||||
"arg2",
|
||||
}
|
||||
end)
|
||||
|
||||
it("supports arguments", function()
|
||||
make_hashbang_file("/test-rom/data/dump-args \"iArg1 iArg1-2\" iArg2")
|
||||
shell.execute("test-files/out.lua", "arg1", "arg2")
|
||||
|
||||
local args = _G.__arg
|
||||
_G.__arg = nil
|
||||
|
||||
expect(args):same {
|
||||
[0] = "/test-rom/data/dump-args",
|
||||
"iArg1 iArg1-2",
|
||||
"iArg2",
|
||||
"test-files/out.lua",
|
||||
"arg1",
|
||||
"arg2",
|
||||
}
|
||||
end)
|
||||
|
||||
it("supports recursion", function()
|
||||
make_hashbang_file("/test-rom/data/dump-args")
|
||||
make_hashbang_file("test-files/out.lua", "test-files/out2.lua")
|
||||
|
||||
shell.execute("test-files/out2.lua", "arg1", "arg2")
|
||||
|
||||
local args = _G.__arg
|
||||
_G.__arg = nil
|
||||
|
||||
expect(args):same {
|
||||
[0] = "/test-rom/data/dump-args",
|
||||
"test-files/out.lua",
|
||||
"test-files/out2.lua",
|
||||
"arg1",
|
||||
"arg2",
|
||||
}
|
||||
end)
|
||||
|
||||
it("returns error for infinite recursion", function()
|
||||
make_hashbang_file("test-files/out.lua")
|
||||
expect(shell.execute("test-files/out.lua")):eq(false)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("shell.run", function()
|
||||
|
Loading…
Reference in New Issue
Block a user