1
0
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:
Emma 2023-01-14 14:12:04 -08:00 committed by GitHub
parent f561572509
commit f43b839056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 34 deletions

View File

@ -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.

View File

@ -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()