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.
|
--[[- The shell API provides access to CraftOS's command line interface.
|
||||||
--
|
|
||||||
-- It allows you to @{run|start programs}, @{setCompletionFunction|add
|
It allows you to @{run|start programs}, @{setCompletionFunction|add completion
|
||||||
-- completion for a program}, and much more.
|
for a program}, and much more.
|
||||||
--
|
|
||||||
-- @{shell} is not a "true" API. Instead, it is a standard program, which injects its
|
@{shell} is not a "true" API. Instead, it is a standard program, which injects
|
||||||
-- API into the programs that it launches. This allows for multiple shells to
|
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
|
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}.
|
environment, and so is unavailable to other @{os.loadAPI|APIs}.
|
||||||
--
|
|
||||||
-- @module[module] shell
|
## 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
|
local make_package = dofile("rom/modules/main/cc/require.lua").make
|
||||||
|
|
||||||
@ -54,6 +79,71 @@ else
|
|||||||
bgColour = colours.black
|
bgColour = colours.black
|
||||||
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
|
||||||
|
|
||||||
|
-- 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.
|
--- Run a program with the supplied arguments.
|
||||||
--
|
--
|
||||||
-- Unlike @{shell.run}, each argument is passed to the program verbatim. While
|
-- 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)
|
multishell.setTitle(multishell.getCurrent(), sTitle)
|
||||||
end
|
end
|
||||||
|
|
||||||
local sDir = fs.getDir(sPath)
|
local result = executeProgram(100, sPath, { [0] = command, ... })
|
||||||
local env = createShellEnv(sDir)
|
|
||||||
env.arg = { [0] = command, ... }
|
|
||||||
local result = os.run(env, sPath, ...)
|
|
||||||
|
|
||||||
tProgramStack[#tProgramStack] = nil
|
tProgramStack[#tProgramStack] = nil
|
||||||
if multishell then
|
if multishell then
|
||||||
@ -108,23 +195,6 @@ function shell.execute(command, ...)
|
|||||||
end
|
end
|
||||||
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
|
-- Install shell API
|
||||||
|
|
||||||
--- Run a program with the supplied arguments.
|
--- Run a program with the supplied arguments.
|
||||||
@ -541,8 +611,8 @@ end
|
|||||||
--- Get the current aliases for this shell.
|
--- Get the current aliases for this shell.
|
||||||
--
|
--
|
||||||
-- Aliases are used to allow multiple commands to refer to a single program. For
|
-- 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
|
-- instance, the `list` program is aliased to `dir` or `ls`. Running `ls`, `dir`
|
||||||
-- `list` in the shell will all run the `list` program.
|
-- or `list` in the shell will all run the `list` program.
|
||||||
--
|
--
|
||||||
-- @treturn { [string] = string } A table, where the keys are the names of
|
-- @treturn { [string] = string } A table, where the keys are the names of
|
||||||
-- aliases, and the values are the path to the program.
|
-- 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" }
|
expect(args):same { [0] = "/test-rom/data/dump-args", "arg1", "arg 2" }
|
||||||
end)
|
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)
|
end)
|
||||||
|
|
||||||
describe("shell.run", function()
|
describe("shell.run", function()
|
||||||
|
Loading…
Reference in New Issue
Block a user