Skip to content

Instantly share code, notes, and snippets.

@dimitriye98
Last active October 13, 2015 06:09
Show Gist options
  • Save dimitriye98/c245e884be0c7362db33 to your computer and use it in GitHub Desktop.
Save dimitriye98/c245e884be0c7362db33 to your computer and use it in GitHub Desktop.
local component = require("component")
local computer = require("computer")
local event = require("event")
local fs = require("filesystem")
local process = require("process")
local shell = require("shell")
local term = require("term")
local text = require("text")
local unicode = require("unicode")
-------------------------------------------------------------------------------
local memoryStream = {}
function memoryStream:close()
self.closed = true
end
function memoryStream:seek()
return nil, "bad file descriptor"
end
function memoryStream:read(n)
if self.closed then
if self.buffer == "" and self.redirect.read then
return self.redirect.read:read(n)
else
return nil -- eof
end
end
if self.buffer == "" then
self.args = table.pack(coroutine.yield(table.unpack(self.result)))
end
local result = string.sub(self.buffer, 1, n)
self.buffer = string.sub(self.buffer, n + 1)
return result
end
function memoryStream:write(value)
if not self.redirect.write and self.closed then
-- if next is dead, ignore all writes
if coroutine.status(self.next) ~= "dead" then
error("attempt to use a closed stream")
end
return true
end
if self.redirect.write then
self.redirect.write:write(value)
end
if not self.closed then
self.buffer = self.buffer .. value
self.result = table.pack(coroutine.resume(self.next, table.unpack(self.args)))
if coroutine.status(self.next) == "dead" then
self:close()
end
if not self.result[1] then
error(self.result[2], 0)
end
table.remove(self.result, 1)
end
return true
end
function memoryStream.new()
local stream = {closed = false, buffer = "",
redirect = {}, result = {}, args = {}}
local metatable = {__index = memoryStream,
__gc = memoryStream.close,
__metatable = "memorystream"}
return setmetatable(stream, metatable)
end
-------------------------------------------------------------------------------
local function clone(tbl, deep)
local out = {}
for k, v in pairs(tbl) do
if deep and type(v) == "table" then
out[k] = clone(v, true)
else
out[k] = v
end
end
return out
end
local function contains(tbl, elem)
for _, v in pairs(tbl) do
if elem == v then
return true
end
end
return false
end
-- Takes a string and escapes it so that it can serve as a pattern literal
local function litPattern(str)
return str:gsub("%W", "%%%0")
end
local operators = {"&&", "||", ";", "\n", "|"} -- must be ordered from long to short
local quotes = {
"${", "$(", '"', "'", "`"; -- array part must be ordered from long to short
['"'] = '"',
["'"] = "'",
["${"] = "}",
["$("] = ")",
["`"] = "`",
-- ["$(("] = "))"
}
local function tokenize(value)
checkArg(1, value, "string")
local i = 0
local len = unicode.len(value)
local function consume(n)
if type(n) == "string" then -- try to consume a predefined token
local begin = i + 1
local final = i + unicode.len(n)
local sub = unicode.sub(value, begin, final)
if sub == n then
i = final
return sub
else
return nil
end
elseif type(n) ~= "number" then -- for for loop iteration
n = 1
end
if len - i <= 0 then
return nil
end
n = math.min(n, len - i)
local begin = i + 1
i = i + n
return unicode.sub(value, begin, i)
end
local function lookahead(n)
if type(n) ~= "number" then
n = 1
end
n = math.min(n, len - i)
local begin = i + 1
local final = i + n
if final - i <= 0 then
return nil
end
return unicode.sub(value, begin, final)
end
local tokens, token = {}, {}
local start, quoted
local lastOp
while lookahead() do
local char = lookahead()
local closed
if quoted then
closed = consume(quotes[quoted])
end
if closed then
table.insert(token, closed)
quoted = nil
elseif quoted == "'" then
table.insert(token, consume(char))
elseif char == "\\" then
table.insert(token, consume(2)) -- backslashes will be removed later
elseif not quoted then
local opened
for _, quote in ipairs(quotes) do
opened = consume(quote)
if opened then
table.insert(token, opened)
quoted = opened
start = i + 1
break
end
end
if not opened then
local isOp
for _, op in ipairs(operators) do
isOp = consume(op)
if isOp then
local tokenOut = table.concat(token)
if tokenOut == "" then
if tokens[#tokens] == lastOp then
return nil, "parse error near '"..isOp.."'"
end
else
table.insert(tokens, tokenOut)
end
table.insert(tokens, isOp)
token = {}
lastOp = isOp
break
end
end
if not isOp then
if char == "#" then -- comment
while lookahead() ~= "\n" do
consume()
end
elseif char:match("%s") then
local tokenOut = table.concat(token)
if tokenOut ~= "" then
table.insert(tokens, table.concat(token))
token = {}
end
consume(char)
else
table.insert(token, consume(char))
end
end
end
else
table.insert(token, consume(char))
end
end
if quoted then
return nil, "unclosed quote at index " .. start, quoted
end
token = table.concat(token)
if token ~= "" then -- insert trailing token
table.insert(tokens, token)
end
return tokens
end
local function nilify(word)
if word == "" then
return nil
else
return word
end
end
local paramOps = {
[":-"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if nilify(value) then
return value
else
return word
end
end,
["-"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if value then
return nilify(value)
else
return word
end
end,
[":="] = function(param, word)
nilify(word)
local value = os.getenv(param)
if nilify(value) then
return value
else
os.setenv(param, word)
return word
end
end,
["="] = function(param, word)
nilify(word)
local value = os.getenv(param)
if value then
return nilify(value)
else
return word
end
end,
[":?"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if nilify(value) then
return value
else
error(word)
end
end,
["?"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if value then
return nilify(value)
else
error(word)
end
end,
[":+"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if nilify(value) then
return word
else
return nil
end
end,
["+"] = function(param, word)
nilify(word)
local value = os.getenv(param)
if value then
return word
else
return nil
end
end
}
local function paramOpPatt(op)
return "(.-)"..op:gsub("%W", "%%%0").."(.*)"
end
local expand -- forward declaration for mutual recursion
local function paramExpand(contents)
for op, handler in pairs(paramOps) do
local param, word = contents:match(paramOpPatt(op))
if param then
if not param:match("%w+") then
error("bad substitution")
end
return handler(param, expand(word))
end
end
return os.getenv(contents)
end
expand = function(value)
local result = value:gsub("%$(%w+)", os.getenv):gsub("%$%b{}",
function(match)
local contents = unicode.sub(match, 3, -2)
if unicode.sub(contents, 1, 1) == "#" then
return tostring(unicode.len(paramExpand(unicode.sub(contents, 2)) or ""))
else
return paramExpand(contents) or match
end
end)
return result
end
local function glob(value)
if not value:match("[^\\]%*") and not value:match("[^\\]%?") then
-- Nothing to do here.
return {expand(value)}
end
local segments = fs.segments(value)
local paths = {value:sub(1, 1) == "/" and "/" or shell.getWorkingDirectory()}
for i, segment in ipairs(segments) do
local nextPaths = {}
local pattern = segment:gsub("([^\\])%*", "%1.*")
:gsub("^%*", ".*")
:gsub("([^\\])%?", "%1.")
:gsub("^%?", ".")
if pattern == segment then
-- Nothing to do, concatenate as-is.
for _, path in ipairs(paths) do
table.insert(nextPaths, fs.concat(path, segment))
end
else
pattern = "^(" .. pattern .. ")/?$"
for _, path in ipairs(paths) do
for file in fs.list(path) do
if file:match(pattern) then
table.insert(nextPaths, fs.concat(path, file))
end
end
end
if #nextPaths == 0 then
error("no matches found: " .. segment)
end
end
paths = nextPaths
end
for i, path in ipairs(paths) do
paths[i] = expand(path)
end
return paths
end
local function consumeQuote(str, open, close)
checkArg(1, str, "string")
checkArg(2, open, "string")
checkArg(3, close, "string")
if str:sub(1, #open) ~= open then
return nil, str
else
local quoted, rest = str:sub(#open + 1):match("(.-[^\\]"..close:gsub("%W", "%%%0")..")(.*)")
if not quoted then
return nil, str
end
return open..quoted, rest
end
end
local function evaluate(value)
local results, remaining = {""}, value
while remaining ~= "" do
local match, rest = consumeQuote(remaining, "'", "'")
if match then -- single quotes; no nested expansion
match = match:sub(2, -2)
remaining = rest
for i,v in ipairs(results) do
results[i] = v..match
end
else
match, rest = consumeQuote(remaining, '"', '"')
if match then
match = match:sub(2, -2)
remaining = rest
else
match, rest = rest:match("(.-[^\\])([\"'].*)")
if match then -- consume until quote
remaining = rest
else -- if there's no opening quote consume the rest
match = remaining
remaining = ""
end
end
local newResults = {}
for _, globbed in ipairs(glob(match)) do
for i, result in ipairs(results) do
table.insert(newResults, result..globbed)
end
end
results = newResults
end
end
-- finally strip the backslashes
for i,v in ipairs(results) do
results[i] = v:gsub("\\(.)", "%1")
end
return results
end
local function parseParams(...)
local params = table.pack(...)
local disables
if type(params[1]) == "table" then
disables = table.remove(params, 1)
end
local args = {}
local options = {}
local doneWithOptions = false
for i = 1, params.n do
local param = params[i]
if not doneWithOptions and type(param) == "string" then
if param == "--" then
doneWithOptions = true -- stop processing options at `--`
elseif unicode.sub(param, 1, 2) == "--" then
if param:match("%-%-(.-)=") ~= nil then
options[param:match("%-%-(.-)=")] = param:match("=(.*)")
else
options[unicode.sub(param, 3)] = true
if disables then
local disable = disables[unicode.sub(param, 3)]
if disable then
if type(disable) == "string" then
options[disable] = nil
elseif type(disable) == "table" then
for _,flag in disable do
options[flag] = nil
end
end
end
end
end
elseif unicode.sub(param, 1, 1) == "-" and param ~= "-" then
for j = 2, unicode.len(param) do
options[unicode.sub(param, j, j)] = true
if disables then
local disable = disables[unicode.sub(param, j, j)]
if disable then
if type(disable) == "string" then
options[disable] = nil
elseif type(disable) == "table" then
for _,flag in disable do
options[flag] = nil
end
end
end
end
end
else
table.insert(args, param)
end
else
table.insert(args, param)
end
end
return args, options
end
local esc = string.char(0x1B)
local builtIns = {
[":"] = function() --[[ Do nothing ]] end;
["source"] = function(_, output, env, fName, ...)
local ret = {eval(io.open(fName), output, env, table.concat({...}, " ") )}
file:close()
return table.unpack(ret)
end;
["eval"] = function(input, output, env, ...)
return eval(input, output, env, table.concat({...}, " "))
end;
["exec"] = function(input, output, env, ...)
-- Unfortunately the flag options seem impossible to implement given
-- the way the process library currently works
local success, code = eval(input, output, env, table.concat({...}, " "))
if success then
os.exit(code)
else
error(code)
end
end;
["echo"] = function(input, output, env, ...)
local args = parseParams({e="E", E="e"}, ...)
local defaultEscapes = os.getenv("echoEscapes")
if defaultEscapes == "false" then
defaultEscapes = false
else
defaultEscapes = true
end
local str = table.concat(args, " ")
if defaultEscapes and (not args.E) or args.e then
local out, escaped, i = {}, false, 0
while i < unicode.len(str) do
i = i + 1
local char = unicode.sub(str, i, i)
if escaped then
if char == "b" then
table.remove(out)
elseif char == "c" then
break
elseif char == "e" or char == "E" then
table.insert(out, esc)
elseif char == "f" then
table.insert(out, "\f")
elseif char == "n" then
table.insert(out, "\n")
elseif char == "r" then
table.insert(out, "\r")
elseif char == "t" then
table.insert(out, "\t")
elseif char == "v" then
table.insert(out, "\v")
elseif char == "\\" then
table.insert(out, "\\")
elseif char == "0" then
local code = tonumber(unicode.sub(str, i+1, i+3), 8)
if code then
table.insert(out, unicode.char(code))
i = i + 3
else
table.insert(out, "\\")
table.insert(out, "0")
end
elseif char == "x" then
local code = tonumber(unicode.sub(str, i+1, i+2), 16)
if code then
table.insert(out, unicode.char(code))
i = i + 2
else
table.insert(out, "\\")
table.insert(out, "x")
end
elseif char == "u" then
local code = str:match("%x%x%x%x") -- 4 hexdecimal digits
if code then
i = i + 4
code = tonumber(code, 16)
table.insert(out, unicode.char(code))
else
table.insert(out, "\\")
table.insert(out, "u")
end
elseif char == "U" then
local code = str:match("%x%x%x%x%x%x%x%x") -- 8 hexdecimal digits
if code then
i = i + 8
code = tonumber(code, 16)
table.insert(out, unicode.char(code))
else
table.insert(out, "\\")
table.insert(out, "u")
end
else
table.insert(out, "\\")
table.insert(out, char)
end
escaped = false
else
if char == "\\" then
escaped = true
else
table.insert(out, char)
end
end
end
str = table.concat(out)
end
output:write(str)
if not args.n then
output:write("\n")
end
end,
["exit"] = true -- Exit needs special processing
}
local function parseCommand(tokens, ...)
if #tokens == 0 then
return
end
local name = tokens[1]
local program, args = shell.resolveAlias(name, table.pack(select(2, table.unpack(tokens))))
local eargs = {}
program = evaluate(program)
for i = 2, #program do
table.insert(eargs, program[i])
end
local reason
if builtIns[program[1]] then
program = program[1]
else
program, reason = shell.resolve(program[1], "lua")
if not program then
return nil, reason
end
end
for i = 1, #args do
for _, arg in ipairs(evaluate(args[i])) do
table.insert(eargs, arg)
end
end
args = eargs
-- Find redirects.
local input, output, mode = nil, nil, "write"
tokens = args
args = {}
local function smt(call) -- state metatable factory
local function index(_, token)
if token == "<" or token == ">" or token == ">>" then
return "parse error near " .. token
end
call(token)
return "args" -- default, return to normal arg parsing
end
return {__index=index}
end
local sm = { -- state machine for redirect parsing
args = setmetatable({["<"]="input", [">"]="output", [">>"]="append"},
smt(function(token)
table.insert(args, token)
end)),
input = setmetatable({}, smt(function(token)
input = token
end)),
output = setmetatable({}, smt(function(token)
output = token
mode = "write"
end)),
append = setmetatable({}, smt(function(token)
output = token
mode = "append"
end))
}
-- Run state machine over tokens.
local state = "args"
for i = 1, #tokens do
local token = tokens[i]
state = sm[state][token]
if not sm[state] then
return nil, state
end
end
return {
program = program,
args = args,
input = input,
output = output,
mode = mode,
name = name
}
end
local function parseCommands(command)
local tokens, reason, quote = tokenize(command)
if not tokens then
return nil, reason, quote
elseif #tokens == 0 then
return true
end
local commands, command = {}, {}
local joinOps = {
["|"] = "pipe",
["&&"] = "and",
["||"] = "or",
[";"] = "delim",
["\n"] = "delim"
}
local lastJoin = "pipe" -- First element is treated as if preceded by a
-- pipe, makes pipeline construction easier
for _, token in ipairs(tokens) do
local join = joinOps[token]
if join then
if #command == 0 then
return nil, "parse error near '"..token.."'"
end
table.insert(commands, {op = lastJoin, command = command})
lastJoin = join
command = {}
else
table.insert(command, token)
end
end
if #command > 0 then -- push tail command
table.insert(commands, {op = lastJoin, command = command})
end
for i, joinCell in pairs(commands) do
local reason
joinCell.command, reason = parseCommand(joinCell.command)
if not joinCell.command then
return nil, reason
end
if joinCell.command.program == nil then
return nil, joinCell.command[2]
end
end
return commands
end
-------------------------------------------------------------------------------
local function runPipeline(input, output, env, pipeline, ...)
if pipeline[1].program == "exit" then
-- If the first command exits further
-- processing is unnecessary.
local code = pipeline[1].args[1]
os.exit(tonumber(code) or code)
end
input = input or io.input()
output = output or io.output()
-- Piping data between programs works like so:
-- program1 gets its output replaced with our custom stream.
-- program2 gets its input replaced with our custom stream.
-- repeat for all programs
-- custom stream triggers execution of 'next' program after write.
-- custom stream triggers yield before read if buffer is empty.
-- custom stream may have 'redirect' entries for fallback/duplication.
local threads, pipes, inputs, outputs = {}, {}, {}, {}
for i = 1, #pipeline do
local command = pipeline[i]
if command.program == "exit" then
threads[i] = {"exit", tonumber(command.args[1]) or command.args[1]}
break
end
local builtIn = builtIns[command.program]
if builtIn then
threads[i] = coroutine.create(function(...)
local input, output = input, output
if command.input then
local file, reason = io.open(shell.resolve(command.input))
if not file then
error("could not open '" .. command.input .. "': " .. reason, 0)
end
table.insert(inputs, file)
if pipes[i - 1] then
pipes[i - 1].stream.redirect.read = file
input = pipes[i - 1]
else
input = file
end
elseif pipes[i - 1] then
input = pipes[i - 1]
end
if command.output then
local file, reason = io.open(shell.resolve(command.output), command.mode == "append" and "a" or "w")
if not file then
error("could not open '" .. command.output .. "': " .. reason, 0)
end
if command.mode == "append" then
io.write("\n")
end
table.insert(outputs, file)
if pipes[i] then
pipes[i].stream.redirect.write = file
output = pipes[i]
else
output = file
end
elseif pipes[i] then
output = pipes[i]
end
output.write("")
return builtIn(input, output, env, ...)
end)
else
local reason
threads[i], reason = process.load(command.program, env, function()
os.setenv("_", command.program)
if command.input then
local file, reason = io.open(shell.resolve(command.input))
if not file then
error("could not open '" .. command.input .. "': " .. reason, 0)
end
table.insert(inputs, file)
if pipes[i - 1] then
pipes[i - 1].stream.redirect.read = file
io.input(pipes[i - 1])
else
io.input(file)
end
elseif pipes[i - 1] then
io.input(pipes[i - 1])
end
if command.output then
local file, reason = io.open(shell.resolve(command.output), command.mode == "append" and "a" or "w")
if not file then
error("could not open '" .. command.output .. "': " .. reason, 0)
end
if command.mode == "append" then
io.write("\n")
end
table.insert(outputs, file)
if pipes[i] then
pipes[i].stream.redirect.write = file
io.output(pipes[i])
else
io.output(file)
end
elseif pipes[i] then
io.output(pipes[i])
end
io.write('')
end, command.name)
end
if not threads[i] then
return false, reason
end
if i < #pipeline and pipeline[i + 1].program ~= "exit" then
pipes[i] = require("buffer").new("rw", memoryStream.new())
pipes[i]:setvbuf("no")
else
pipes[i] = output
end
if i > 1 then
pipes[i - 1].stream.next = threads[i]
pipes[i - 1].stream.args = command.args
else
pipes[i - 1] = input
end
end
local args = pipeline[1].args
for _, arg in ipairs(table.pack(...)) do
table.insert(args, arg)
end
table.insert(args, 1, true)
args.n = #args
local result = nil
for i = 1, #threads do
if type(threads[i]) == "table" and threads[i][1] == "exit" then -- Exit logic
os.exit(threads[i][2])
end
-- Emulate CC behavior by making yields a filtered event.pull()
while args[1] and coroutine.status(threads[i]) ~= "dead" do
result = table.pack(coroutine.resume(threads[i], table.unpack(args, 2, args.n)))
if coroutine.status(threads[i]) ~= "dead" then
args = table.pack(pcall(event.pull, table.unpack(result, 2, result.n)))
end
end
if pipes[i] then
pcall(pipes[i].close, pipes[i])
end
if not result[1] then
if type(result[2]) == "table" and result[2].reason == "terminated" then
result[1] = true
result[2] = result[2].code
result.n = 2
elseif type(result[2]) == "string" then
result[2] = debug.traceback(threads[i], result[2])
break
end
end
end
-- copy env vars from last process; mostly to ensure stuff like cd.lua works
local lastVars = rawget(process.info(threads[#threads]).data, "vars")
if lastVars then
local localVars = process.info().data.vars
for k,v in pairs(lastVars) do
localVars[k] = v
end
end
for _, input in ipairs(inputs) do
input:close()
end
for _, output in ipairs(outputs) do
output:close()
end
if not args[1] then
return false, args[2]
end
return table.unpack(result, 1, result.n)
end
local function eval(input, output, env, command, ...)
checkArg(4, command, "string")
local commands, reason, quote = parseCommands(command)
if not commands then
return false, reason, quote
end
if #commands == 0 then
return true
end
-- Build pipelines
local pipelines, pipeline = {}, {}
local count = 0
for i, joinCell in ipairs(commands) do
if joinCell.op ~= "pipe" then
table.insert(pipelines, {op = joinCell.op, pipeline = pipeline})
pipeline = {}
count = count + 1
end
table.insert(pipeline, joinCell.command)
end
if #pipeline > 0 then -- add tail pipeline
table.insert(pipelines, {op = "delim", pipeline = pipeline})
end
local out
-- Run pipelines
for i, pipeCell in ipairs(pipelines) do
local op, pipeline = pipeCell.op, pipeCell.pipeline
local results = {runPipeline(input, output, env, pipeline, ...)}
local success = results[1] and (results[2] == nil or results[2] == true or results[2] == 0)
if success and op == "or" then
return table.unpack(results)
elseif op == "and" then
return table.unpack(results)
end
out = results
end
return table.unpack(out)
end
local function execute(env, command, ...)
checkArg(2, command, "string")
return eval(nil, nil, env, command, ...)
end
local args, options = shell.parse(...)
local history = {}
local function escapeMagic(text)
return text:gsub('[%(%)%.%%%+%-%*%?%[%^%$]', '%%%1')
end
local function getMatchingPrograms(baseName)
local result = {}
-- TODO only matching files with .lua extension for now, might want to
-- extend this to other extensions at some point? env var? file attrs?
if not baseName or #baseName == 0 then
baseName = "^(.*)%.lua$"
else
baseName = "^(" .. escapeMagic(baseName) .. ".*)%.lua$"
end
for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do
for file in fs.list(basePath) do
local match = file:match(baseName)
if match then
table.insert(result, match)
end
end
end
return result
end
local function getMatchingFiles(basePath, name)
local resolvedPath = shell.resolve(basePath)
local result, baseName = {}
-- note: we strip the trailing / to make it easier to navigate through
-- directories using tab completion (since entering the / will then serve
-- as the intention to go into the currently hinted one).
-- if we have a directory but no trailing slash there may be alternatives
-- on the same level, so don't look inside that directory... (cont.)
if fs.isDirectory(resolvedPath) and name:len() == 0 then
baseName = "^(.-)/?$"
else
baseName = "^(" .. escapeMagic(name) .. ".-)/?$"
end
for file in fs.list(resolvedPath) do
local match = file:match(baseName)
if match then
table.insert(result, basePath .. match)
end
end
-- (cont.) but if there's only one match and it's a directory, *then* we
-- do want to add the trailing slash here.
if #result == 1 and fs.isDirectory(result[1]) then
result[1] = result[1] .. "/"
end
return result
end
local function hintHandler(line, cursor)
local line = unicode.sub(line, 1, cursor - 1)
if not line or #line < 1 then
return nil
end
local result
local prefix, partial = string.match(line, "^(.+%s)(.+)$")
local searchInPath = not prefix and not line:find("/")
if searchInPath then
-- first part and no path, look for programs in the $PATH
result = getMatchingPrograms(line)
else -- just look normal files
local partialPrefix = (partial or line)
local name = partialPrefix:gsub("/+", "/")
name = name:sub(-1) == '/' and '' or fs.name(name)
partialPrefix = partialPrefix:sub(1, -name:len() - 1)
result = getMatchingFiles(partialPrefix, name)
end
local resultSuffix = ""
if searchInPath then
resultSuffix = " "
elseif #result == 1 and result[1]:sub(-1) ~= '/' then
resultSuffix = " "
end
prefix = prefix or ""
for i = 1, #result do
result[i] = prefix .. result[i] .. resultSuffix
end
table.sort(result)
return result
end
if args.version then
return print("OpenComputers dsh 0.1.0")
end
local quotePrefixes = {["'"] = "quote> ", ["\""] = "dquote> ", ["`"] = "bquote> "}
if options.c then
-- noninteractive from commandline
local result = table.pack(execute(nil, args[1])) --TODO capture parameters into numbered params
if not result[1] then
io.stderr:write(result[2], 0)
end
return table.unpack(result, 2)
elseif (io.input() == io.stdin or options.i) then
-- interactive shell.
while true do
if not term.isAvailable() then -- don't clear unless we lost the term
while not term.isAvailable() do
event.pull("term_available")
end
term.clear()
end
local accumulator = ""
local unmatchedQuote
while term.isAvailable() do
local foreground = component.gpu.setForeground(0xFF0000)
if unmatchedQuote then
term.write(quotePrefixes[unmatchedQuote] or "> ")
else
term.write(expand(os.getenv("PS1") or "$ "))
end
component.gpu.setForeground(foreground)
accumulator = accumulator..term.read(history, nil, hintHandler)
while #history > (tonumber(os.getenv("HISTSIZE")) or 10) do
table.remove(history, 1)
end
if accumulator ~= "" then
local result, reason, quote = execute(nil, accumulator)
if term.getCursor() > 1 then
term.write("\n")
end
if result then
accumulator = ""
unmatchedQuote = nil
else
if reason:match("unclosed quote") then
unmatchedQuote = quote
accumulator = accumulator.."\n"
else
io.stderr:write((reason and tostring(reason) or "unknown error") .. "\n")
accumulator = ""
unmatchedQuote = nil
end
end
end
end
end
elseif io.input() ~= io.stdin then
local result = table.pack(execute(nil, io.read("*a"), ...))
if not result[1] then
io.stderr:write(result[2])
end
return table.unpack(result, 2)
else
-- execute command.
local result = table.pack(execute(...))
if result[1] == "exit" then
table.remove(result, 1)
end
if not result[1] then
error(result[2], 0)
end
return table.unpack(result, 2)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment