CC-Tweaked/src/main/resources/data/computercraft/lua/rom/programs/shell.lua

616 lines
20 KiB
Lua

--- 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 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
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
local parentTerm = term.current()
if multishell then
multishell.setTitle(multishell.getCurrent(), "shell")
end
local bExit = false
local sDir = parentShell and parentShell.dir() or ""
local sPath = parentShell and parentShell.path() or ".:/rom/programs"
local tAliases = parentShell and parentShell.aliases() or {}
local tCompletionInfo = parentShell and parentShell.getCompletionInfo() or {}
local tProgramStack = {}
local shell = {} --- @export
local function createShellEnv(dir)
local env = { shell = shell, multishell = multishell }
env.require, env.package = make_package(env, dir)
return env
end
-- Colours
local promptColour, textColour, bgColour
if term.isColour() then
promptColour = colours.yellow
textColour = colours.white
bgColour = colours.black
else
promptColour = colours.white
textColour = colours.white
bgColour = colours.black
end
--- Run a program with the supplied arguments.
--
-- Unlike @{shell.run}, each argument is passed to the program verbatim. While
-- `shell.run("echo", "b c")` runs `echo` with `b` and `c`,
-- `shell.execute("echo", "b c")` runs `echo` with a single argument `b c`.
--
-- @tparam string command The program to execute.
-- @tparam string ... Arguments to this program.
-- @treturn boolean Whether the program exited successfully.
-- @usage Run `paint my-image` from within your program:
--
-- shell.execute("paint", "my-image")
function shell.execute(command, ...)
expect(1, command, "string")
for i = 1, select('#', ...) do
expect(i + 1, select(i, ...), "string")
end
local sPath = shell.resolveProgram(command)
if sPath ~= nil then
tProgramStack[#tProgramStack + 1] = sPath
if multishell then
local sTitle = fs.getName(sPath)
if sTitle:sub(-4) == ".lua" then
sTitle = sTitle:sub(1, -5)
end
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, ...)
tProgramStack[#tProgramStack] = nil
if multishell then
if #tProgramStack > 0 then
local sTitle = fs.getName(tProgramStack[#tProgramStack])
if sTitle:sub(-4) == ".lua" then
sTitle = sTitle:sub(1, -5)
end
multishell.setTitle(multishell.getCurrent(), sTitle)
else
multishell.setTitle(multishell.getCurrent(), "shell")
end
end
return result
else
printError("No such program")
return false
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.
--
-- All arguments are concatenated together and then parsed as a command line. As
-- a result, `shell.run("program a b")` is the same as `shell.run("program",
-- "a", "b")`.
--
-- @tparam string ... The program to run and its arguments.
-- @treturn boolean Whether the program exited successfully.
-- @usage Run `paint my-image` from within your program:
--
-- shell.run("paint", "my-image")
-- @see shell.execute Run a program directly without parsing the arguments.
function shell.run(...)
local tWords = tokenise(...)
local sCommand = tWords[1]
if sCommand then
return shell.execute(sCommand, table.unpack(tWords, 2))
end
return false
end
--- Exit the current shell.
--
-- This does _not_ terminate your program, it simply makes the shell terminate
-- after your program has finished. If this is the toplevel shell, then the
-- computer will be shutdown.
function shell.exit()
bExit = true
end
--- Return the current working directory. This is what is displayed before the
-- `> ` of the shell prompt, and is used by @{shell.resolve} to handle relative
-- paths.
--
-- @treturn string The current working directory.
-- @see setDir To change the working directory.
function shell.dir()
return sDir
end
--- Set the current working directory.
--
-- @tparam string dir The new working directory.
-- @throws If the path does not exist or is not a directory.
-- @usage Set the working directory to "rom"
--
-- shell.setDir("rom")
function shell.setDir(dir)
expect(1, dir, "string")
if not fs.isDir(dir) then
error("Not a directory", 2)
end
sDir = fs.combine(dir, "")
end
--- Set the path where programs are located.
--
-- The path is composed of a list of directory names in a string, each separated
-- by a colon (`:`). On normal turtles will look in the current directory (`.`),
-- `/rom/programs` and `/rom/programs/turtle` folder, making the path
-- `.:/rom/programs:/rom/programs/turtle`.
--
-- @treturn string The current shell's path.
-- @see setPath To change the current path.
function shell.path()
return sPath
end
--- Set the @{path|current program path}.
--
-- Be careful to prefix directories with a `/`. Otherwise they will be searched
-- for from the @{shell.dir|current directory}, rather than the computer's root.
--
-- @tparam string path The new program path.
function shell.setPath(path)
expect(1, path, "string")
sPath = path
end
--- Resolve a relative path to an absolute path.
--
-- The @{fs} and @{io} APIs work using absolute paths, and so we must convert
-- any paths relative to the @{dir|current directory} to absolute ones. This
-- does nothing when the path starts with `/`.
--
-- @tparam string path The path to resolve.
-- @usage Resolve `startup.lua` when in the `rom` folder.
--
-- shell.setDir("rom")
-- print(shell.resolve("startup.lua"))
-- -- => rom/startup.lua
function shell.resolve(path)
expect(1, path, "string")
local sStartChar = string.sub(path, 1, 1)
if sStartChar == "/" or sStartChar == "\\" then
return fs.combine("", path)
else
return fs.combine(sDir, path)
end
end
local function pathWithExtension(_sPath, _sExt)
local nLen = #sPath
local sEndChar = string.sub(_sPath, nLen, nLen)
-- Remove any trailing slashes so we can add an extension to the path safely
if sEndChar == "/" or sEndChar == "\\" then
_sPath = string.sub(_sPath, 1, nLen - 1)
end
return _sPath .. "." .. _sExt
end
--- Resolve a program, using the @{path|program path} and list of @{aliases|aliases}.
--
-- @tparam string command The name of the program
-- @treturn string|nil The absolute path to the program, or @{nil} if it could
-- not be found.
-- @usage Locate the `hello` program.
--
-- shell.resolveProgram("hello")
-- -- => rom/programs/fun/hello.lua
function shell.resolveProgram(command)
expect(1, command, "string")
-- Substitute aliases firsts
if tAliases[command] ~= nil then
command = tAliases[command]
end
-- If the path is a global path, use it directly
if command:find("/") or command:find("\\") then
local sPath = shell.resolve(command)
if fs.exists(sPath) and not fs.isDir(sPath) then
return sPath
else
local sPathLua = pathWithExtension(sPath, "lua")
if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
return sPathLua
end
end
return nil
end
-- Otherwise, look on the path variable
for sPath in string.gmatch(sPath, "[^:]+") do
sPath = fs.combine(shell.resolve(sPath), command)
if fs.exists(sPath) and not fs.isDir(sPath) then
return sPath
else
local sPathLua = pathWithExtension(sPath, "lua")
if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
return sPathLua
end
end
end
-- Not found
return nil
end
--- Return a list of all programs on the @{shell.path|path}.
--
-- @tparam[opt] boolean include_hidden Include hidden files. Namely, any which
-- start with `.`.
-- @treturn { string } A list of available programs.
-- @usage textutils.tabulate(shell.programs())
function shell.programs(include_hidden)
expect(1, include_hidden, "boolean", "nil")
local tItems = {}
-- Add programs from the path
for sPath in string.gmatch(sPath, "[^:]+") do
sPath = shell.resolve(sPath)
if fs.isDir(sPath) then
local tList = fs.list(sPath)
for n = 1, #tList do
local sFile = tList[n]
if not fs.isDir(fs.combine(sPath, sFile)) and
(include_hidden or string.sub(sFile, 1, 1) ~= ".") then
if #sFile > 4 and sFile:sub(-4) == ".lua" then
sFile = sFile:sub(1, -5)
end
tItems[sFile] = true
end
end
end
end
-- Sort and return
local tItemList = {}
for sItem in pairs(tItems) do
table.insert(tItemList, sItem)
end
table.sort(tItemList)
return tItemList
end
local function completeProgram(sLine)
if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then
-- Add programs from the root
return fs.complete(sLine, sDir, true, false)
else
local tResults = {}
local tSeen = {}
-- Add aliases
for sAlias in pairs(tAliases) do
if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then
local sResult = string.sub(sAlias, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult)
tSeen[sResult] = true
end
end
end
-- Add all subdirectories. We don't include files as they will be added in the block below
local tDirs = fs.complete(sLine, sDir, false, false)
for i = 1, #tDirs do
local sResult = tDirs[i]
if not tSeen[sResult] then
table.insert (tResults, sResult)
tSeen [sResult] = true
end
end
-- Add programs from the path
local tPrograms = shell.programs()
for n = 1, #tPrograms do
local sProgram = tPrograms[n]
if #sProgram > #sLine and string.sub(sProgram, 1, #sLine) == sLine then
local sResult = string.sub(sProgram, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult)
tSeen[sResult] = true
end
end
end
-- Sort and return
table.sort(tResults)
return tResults
end
end
local function completeProgramArgument(sProgram, nArgument, sPart, tPreviousParts)
local tInfo = tCompletionInfo[sProgram]
if tInfo then
return tInfo.fnComplete(shell, nArgument, sPart, tPreviousParts)
end
return nil
end
--- Complete a shell command line.
--
-- This accepts an incomplete command, and completes the program name or
-- arguments. For instance, `l` will be completed to `ls`, and `ls ro` will be
-- completed to `ls rom/`.
--
-- Completion handlers for your program may be registered with
-- @{shell.setCompletionFunction}.
--
-- @tparam string sLine The input to complete.
-- @treturn { string }|nil The list of possible completions.
-- @see _G.read For more information about completion.
-- @see shell.completeProgram
-- @see shell.setCompletionFunction
-- @see shell.getCompletionInfo
function shell.complete(sLine)
expect(1, sLine, "string")
if #sLine > 0 then
local tWords = tokenise(sLine)
local nIndex = #tWords
if string.sub(sLine, #sLine, #sLine) == " " then
nIndex = nIndex + 1
end
if nIndex == 1 then
local sBit = tWords[1] or ""
local sPath = shell.resolveProgram(sBit)
if tCompletionInfo[sPath] then
return { " " }
else
local tResults = completeProgram(sBit)
for n = 1, #tResults do
local sResult = tResults[n]
local sPath = shell.resolveProgram(sBit .. sResult)
if tCompletionInfo[sPath] then
tResults[n] = sResult .. " "
end
end
return tResults
end
elseif nIndex > 1 then
local sPath = shell.resolveProgram(tWords[1])
local sPart = tWords[nIndex] or ""
local tPreviousParts = tWords
tPreviousParts[nIndex] = nil
return completeProgramArgument(sPath , nIndex - 1, sPart, tPreviousParts)
end
end
return nil
end
--- Complete the name of a program.
--
-- @tparam string program The name of a program to complete.
-- @treturn { string } A list of possible completions.
-- @see cc.shell.completion.program
function shell.completeProgram(program)
expect(1, program, "string")
return completeProgram(program)
end
--- Set the completion function for a program. When the program is entered on
-- the command line, this program will be called to provide auto-complete
-- information.
--
-- The completion function accepts four arguments:
--
-- 1. The current shell. As completion functions are inherited, this is not
-- guaranteed to be the shell you registered this function in.
-- 2. The index of the argument currently being completed.
-- 3. The current argument. This may be the empty string.
-- 4. A list of the previous arguments.
--
-- For instance, when completing `pastebin put rom/st` our pastebin completion
-- function will receive the shell API, an index of 2, `rom/st` as the current
-- argument, and a "previous" table of `{ "put" }`. This function may then wish
-- to return a table containing `artup.lua`, indicating the entire command
-- should be completed to `pastebin put rom/startup.lua`.
--
-- You completion entries may also be followed by a space, if you wish to
-- indicate another argument is expected.
--
-- @tparam string program The path to the program. This should be an absolute path
-- _without_ the leading `/`.
-- @tparam function(shell: table, index: number, argument: string, previous: { string }):({ string }|nil) complete
-- The completion function.
-- @see cc.shell.completion Various utilities to help with writing completion functions.
-- @see shell.complete
-- @see _G.read For more information about completion.
function shell.setCompletionFunction(program, complete)
expect(1, program, "string")
expect(2, complete, "function")
tCompletionInfo[program] = {
fnComplete = complete,
}
end
--- Get a table containing all completion functions.
--
-- This should only be needed when building custom shells. Use
-- @{setCompletionFunction} to add a completion function.
--
-- @treturn { [string] = { fnComplete = function } } A table mapping the
-- absolute path of programs, to their completion functions.
function shell.getCompletionInfo()
return tCompletionInfo
end
--- Returns the path to the currently running program.
--
-- @treturn string The absolute path to the running program.
function shell.getRunningProgram()
if #tProgramStack > 0 then
return tProgramStack[#tProgramStack]
end
return nil
end
--- Add an alias for a program.
--
-- @tparam string command The name of the alias to add.
-- @tparam string program The name or path to the program.
-- @usage Alias `vim` to the `edit` program
--
-- shell.setAlias("vim", "edit")
function shell.setAlias(command, program)
expect(1, command, "string")
expect(2, program, "string")
tAliases[command] = program
end
--- Remove an alias.
--
-- @tparam string command The alias name to remove.
function shell.clearAlias(command)
expect(1, command, "string")
tAliases[command] = nil
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.
--
-- @treturn { [string] = string } A table, where the keys are the names of
-- aliases, and the values are the path to the program.
-- @see shell.setAlias
-- @see shell.resolveProgram This uses aliases when resolving a program name to
-- an absolute path.
function shell.aliases()
-- Copy aliases
local tCopy = {}
for sAlias, sCommand in pairs(tAliases) do
tCopy[sAlias] = sCommand
end
return tCopy
end
if multishell then
--- Open a new @{multishell} tab running a command.
--
-- This behaves similarly to @{shell.run}, but instead returns the process
-- index.
--
-- This function is only available if the @{multishell} API is.
--
-- @tparam string ... The command line to run.
-- @see shell.run
-- @see multishell.launch
-- @usage Launch the Lua interpreter and switch to it.
--
-- local id = shell.openTab("lua")
-- shell.switchTab(id)
function shell.openTab(...)
local tWords = tokenise(...)
local sCommand = tWords[1]
if sCommand then
local sPath = shell.resolveProgram(sCommand)
if sPath == "rom/programs/shell.lua" then
return multishell.launch(createShellEnv("rom/programs"), sPath, table.unpack(tWords, 2))
elseif sPath ~= nil then
return multishell.launch(createShellEnv("rom/programs"), "rom/programs/shell.lua", sCommand, table.unpack(tWords, 2))
else
printError("No such program")
end
end
end
--- Switch to the @{multishell} tab with the given index.
--
-- @tparam number id The tab to switch to.
-- @see multishell.setFocus
function shell.switchTab(id)
expect(1, id, "number")
multishell.setFocus(id)
end
end
local tArgs = { ... }
if #tArgs > 0 then
-- "shell x y z"
-- Run the program specified on the commandline
shell.run(...)
else
-- "shell"
-- Print the header
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
print(os.version())
term.setTextColour(textColour)
-- Run the startup program
if parentShell == nil then
shell.run("/rom/startup.lua")
end
-- Read commands and execute them
local tCommandHistory = {}
while not bExit do
term.redirect(parentTerm)
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
write(shell.dir() .. "> ")
term.setTextColour(textColour)
local sLine
if settings.get("shell.autocomplete") then
sLine = read(nil, tCommandHistory, shell.complete)
else
sLine = read(nil, tCommandHistory)
end
if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then
table.insert(tCommandHistory, sLine)
end
shell.run(sLine)
end
end