fixed output rendering in term.write a bit; cleaned up shell execution logic a little (shell.execute now loads the $SHELL directly and runs it with the command that should be performed); added option for a read timeout to buffer:read; primitive variable expansion for default shell (no support for escaped quotes/brackets)

This commit is contained in:
Florian Nücke 2014-02-24 11:17:05 +01:00
parent c71646569b
commit 05f6fcfebf
21 changed files with 205 additions and 135 deletions

View File

@ -134,7 +134,7 @@ class InternetCard extends ManagedComponent {
def isTcpEnabled(context: Context, args: Arguments): Array[AnyRef] = result(Settings.get.httpEnabled)
@Callback(doc = """function(address:string[, port:number]):number -- Opens a new TCP connection. Returns the handle of the connection.""")
def connect(context: Context, args: Arguments): Array[AnyRef] = {
def connect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val address = args.checkString(0)
val port = if (args.count > 1) args.checkInteger(1) else -1
if (!Settings.get.tcpEnabled) {
@ -153,8 +153,8 @@ class InternetCard extends ManagedComponent {
result(handle)
}
@Callback(doc = """function(handle:number) -- Closes an open socket stream.""")
def close(context: Context, args: Arguments): Array[AnyRef] = {
@Callback(direct = true, doc = """function(handle:number) -- Closes an open socket stream.""")
def close(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val handle = args.checkInteger(0)
connections.remove(handle) match {
case Some(socket) => socket.close()
@ -164,7 +164,7 @@ class InternetCard extends ManagedComponent {
}
@Callback(doc = """function(handle:number, data:string):number -- Tries to write data to the socket stream. Returns the number of bytes written.""")
def write(context: Context, args: Arguments): Array[AnyRef] = {
def write(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val handle = args.checkInteger(0)
val value = args.checkByteArray(1)
connections.get(handle) match {
@ -176,7 +176,7 @@ class InternetCard extends ManagedComponent {
}
@Callback(doc = """function(handle:number, n:number):string -- Tries to read data from the socket stream. Returns the read byte array.""")
def read(context: Context, args: Arguments): Array[AnyRef] = {
def read(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val handle = args.checkInteger(0)
val n = math.min(Settings.get.maxReadBuffer, math.max(0, args.checkInteger(1)))
connections.get(handle) match {
@ -222,7 +222,7 @@ class InternetCard extends ManagedComponent {
}
}
override def onDisconnect(node: Node) {
override def onDisconnect(node: Node) = this.synchronized {
super.onDisconnect(node)
if (owner.isDefined && (node == this.node || node.host.isInstanceOf[Context] && (node.host.asInstanceOf[Context] == owner.get))) {
owner = None
@ -236,7 +236,7 @@ class InternetCard extends ManagedComponent {
}
}
override def onMessage(message: Message) {
override def onMessage(message: Message) = this.synchronized {
super.onMessage(message)
message.data match {
case Array() if (message.name == "computer.stopped" || message.name == "computer.started") && owner.isDefined && message.source.address == owner.get.node.address =>

View File

@ -16,7 +16,7 @@ local args, options = shell.parse(...)
local function get(pasteId, filename)
local f, reason = io.open(filename, "w")
if not f then
io.stderr:write("Failed opening file for writing: ", reason)
io.stderr:write("Failed opening file for writing: " .. reason)
return
end
@ -33,12 +33,12 @@ local function get(pasteId, filename)
end
f:close()
io.write("Saved data to ", filename, "\n")
io.write("Saved data to " .. filename .. "\n")
else
io.write("failed.\n")
f:close()
fs.remove(filename)
io.stderr:write("HTTP request failed: ", response, "\n")
io.stderr:write("HTTP request failed: " .. response .. "\n")
end
end
@ -48,7 +48,7 @@ function encode(code)
code = string.gsub(code, "([^%w ])", function (c)
return string.format("%%%02X", string.byte(c))
end)
code = string.gsub (code, " ", "+")
code = string.gsub(code, " ", "+")
end
return code
end
@ -74,14 +74,14 @@ function put(path)
if configFile then
local result, reason = pcall(configFile)
if not result then
io.stderr:write("Failed loading config: ", reason)
io.stderr:write("Failed loading config: " .. reason)
end
end
config.key = config.key or "fd92bd40a84c127eeb6804b146793c97"
local file, reason = io.open(path, "r")
if not file then
io.stderr:write("Failed opening file for reading: ", reason)
io.stderr:write("Failed opening file for reading: " .. reason)
return
end
@ -109,8 +109,8 @@ function put(path)
else
io.write("success.\n")
local pasteId = string.match(info, "[^/]+$")
io.write("Uploaded as ", info, "\n")
io.write('Run "pastebin get ', pasteId, '" to download anywhere.')
io.write("Uploaded as " .. info .. "\n")
io.write('Run "pastebin get ' .. pasteId .. '" to download anywhere.')
end
else
io.write("failed.\n")

View File

@ -47,7 +47,7 @@ end
local f, reason = io.open(filename, "wb")
if not f then
io.stderr:write("failed opening file for writing: ", reason)
io.stderr:write("failed opening file for writing: " .. reason)
return
end
@ -65,7 +65,7 @@ if result then
f:close()
if not options.q then
io.write("Saved data to ", filename, "\n")
io.write("Saved data to " .. filename .. "\n")
end
else
if not options.q then
@ -73,5 +73,5 @@ else
end
f:close()
fs.remove(filename)
io.stderr:write("HTTP request failed: ", response, "\n")
io.stderr:write("HTTP request failed: " .. response .. "\n")
end

View File

@ -109,13 +109,7 @@ function internet.socket(address, port)
-- the __gc metamethod. So we start a timer to do the yield/cleanup.
local function cleanup(self)
if not self.handle then return end
-- save non-gc'ed values as upvalues
local inet = self.inet
local handle = self.handle
local function close()
inet.close(handle)
end
event.timer(0, close)
pcall(self.inet.close, self.handle)
end
local metatable = {__index = socketStream,
__gc = cleanup,

View File

@ -4,7 +4,7 @@ local args = shell.parse(...)
if #args == 0 then
for name, value in shell.aliases() do
io.write(name, " ", value, "\n")
io.write(name .. " " .. value .. "\n")
end
elseif #args == 1 then
local value = shell.getAlias(args[1])
@ -15,5 +15,5 @@ elseif #args == 1 then
end
else
shell.setAlias(args[1], args[2])
io.write("alias created: ", args[1], " -> ", args[2])
io.write("alias created: " .. args[1] .. " -> " .. args[2])
end

View File

@ -253,7 +253,7 @@ local function execute(command, env, ...)
for i = 1, #commands do
local program, args, input, output, mode = table.unpack(commands[i])
local reason
threads[i], reason = shell.load(program, env, function()
threads[i], reason = process.load(shell.resolve(program, "lua"), env, function()
if input then
local file, reason = io.open(shell.resolve(input))
if not file then

View File

@ -1,5 +1,5 @@
local component = require("component")
for address, name in component.list() do
io.write(name, "\t", address, "\n")
io.write(name .. "\t" .. address .. "\n")
end

View File

@ -18,7 +18,7 @@ for i = 1, #dirs do
end
local list, reason = fs.list(path)
if not list then
io.write(reason, "\n")
io.write(reason .. "\n")
else
local function setColor(c)
if component.gpu.getForeground() ~= c then
@ -40,7 +40,7 @@ for i = 1, #dirs do
setColor(0x99CCFF)
for _, d in ipairs(lsd) do
if options.a or d:sub(1, 1) ~= "." then
io.write(d, "\t")
io.write(d .. "\t")
if options.l or io.output() ~= io.stdout then
io.write("\n")
end
@ -55,7 +55,7 @@ for i = 1, #dirs do
setColor(0xFFFFFF)
end
if options.a or f:sub(1, 1) ~= "." then
io.write(f, "\t")
io.write(f .. "\t")
if options.l then
setColor(0xFFFFFF)
io.write(fs.size(fs.concat(path, f)), "\n")

View File

@ -16,11 +16,11 @@ local env = setmetatable({}, {__index = function(t, k)
end})
component.gpu.setForeground(0xFFFFFF)
io.write("Lua 5.2.3 Copyright (C) 1994-2013 Lua.org, PUC-Rio\n")
term.write("Lua 5.2.3 Copyright (C) 1994-2013 Lua.org, PUC-Rio\n")
component.gpu.setForeground(0xFFFF00)
io.write("Enter a statement and hit enter to evaluate it.\n")
io.write("Prefix an expression with '=' to show its value.\n")
io.write("Press Ctrl+C to exit the interpreter.\n")
term.write("Enter a statement and hit enter to evaluate it.\n")
term.write("Prefix an expression with '=' to show its value.\n")
term.write("Press Ctrl+C to exit the interpreter.\n")
component.gpu.setForeground(0xFFFFFF)
while term.isAvailable() do
@ -46,16 +46,16 @@ while term.isAvailable() do
if type(result[2]) == "table" and result[2].reason == "terminated" then
os.exit(result[2].code)
end
io.stderr:write(tostring(result[2]), "\n")
io.stderr:write(tostring(result[2]) .. "\n")
else
for i = 2, result.n do
io.write(text.serialize(result[i], true), "\t")
term.write(text.serialize(result[i], true) .. "\t")
end
if result.n > 1 then
io.write("\n")
if term.getCursor() > 1 then
term.write("\n")
end
end
else
io.stderr:write(reason, "\n")
io.stderr:write(tostring(reason) .. "\n")
end
end

View File

@ -18,6 +18,6 @@ for i = 1, #args do
reason = "unknown reason"
end
end
io.stderr:write(path, ": ", reason, "\n")
io.stderr:write(path .. ": " .. reason .. "\n")
end
end

View File

@ -50,8 +50,8 @@ if options.b then
end
rs.setBundledOutput(side, color, value)
end
io.write("in: ", rs.getBundledInput(side, color), "\n")
io.write("out: ", rs.getBundledOutput(side, color))
io.write("in: " .. rs.getBundledInput(side, color) .. "\n")
io.write("out: " .. rs.getBundledOutput(side, color))
else
if #args > 1 then
local value = args[2]
@ -62,6 +62,6 @@ else
end
rs.setOutput(side, value)
end
io.write("in: ", rs.getInput(side), "\n")
io.write("out: ", rs.getOutput(side))
io.write("in: " .. rs.getInput(side) .. "\n")
io.write("out: " .. rs.getOutput(side))
end

View File

@ -9,6 +9,6 @@ end
for i = 1, #args do
local path = shell.resolve(args[i])
if not os.remove(path) then
io.stderr:write(path, ": no such file, or permission denied\n")
io.stderr:write(path .. ": no such file, or permission denied\n")
end
end

View File

@ -2,7 +2,7 @@ local args = {...}
if #args < 1 then
for k,v in pairs(os.getenv()) do
io.write(k, "='", string.gsub(v, "'", [['"'"']]), "'\n")
io.write(k .. "='" .. string.gsub(v, "'", [['"'"']]) .. "'\n")
end
else
local count = 0

View File

@ -6,45 +6,133 @@ local shell = require("shell")
local term = require("term")
local text = require("text")
local args, options = shell.parse(...)
local history = {}
if options.v or not process.running(2) then
io.write(_OSVERSION .. " (" .. math.floor(computer.totalMemory() / 1024) .. "k RAM)\n")
local function expand(value)
return value:gsub("%$(%w+)", os.getenv):gsub("%$%b{}",
function(match) return os.getenv(expand(match:sub(3, -2))) or match end)
end
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()
if options.v then
io.write(_OSVERSION .. " (" .. math.floor(computer.totalMemory() / 1024) .. "k RAM)\n")
local function evaluate(value)
local init, result = 1, ""
repeat
local match = value:match("^%b''", init)
if match then -- single quoted string. no variable expansion.
match = match:sub(2, -2)
init = init + 2
result = result .. match
else
match = value:match('^%b""', init)
if match then -- double quoted string.
match = match:sub(2, -2)
init = init + 2
else
-- plaintext?
match = value:match("^([^']+)%b''", init)
if not match then -- unmatched single quote.
match = value:match('^([^"]+)%b""', init)
if not match then -- unmatched double quote.
match = value:sub(init)
end
end
end
result = result .. expand(match)
end
init = init + #match
until init > #value
return result
end
local function execute(command, ...)
local parts, reason = text.tokenize(command)
if not parts then
return false, reason
elseif #parts == 0 then
return true
end
while term.isAvailable() do
local foreground = component.gpu.setForeground(0xFF0000)
term.write(os.getenv("PS1") or "# ")
component.gpu.setForeground(foreground)
local command = term.read(history)
if not command then
io.write("exit\n")
return -- eof
end
while #history > 10 do
table.remove(history, 1)
end
command = text.trim(command)
if command == "exit" then
return
elseif command ~= "" then
local result, reason = os.execute(command)
if not result then
io.stderr:write(reason .. "\n")
elseif term.getCursor() > 1 then
io.write("\n")
local program, args = shell.resolveAlias(parts[1], table.pack(select(2, table.unpack(parts))))
program = evaluate(program)
local program, reason = shell.resolve(program, "lua")
if not program then
return false, reason
end
for i = 1, args.n do
args[i] = evaluate(args[i])
end
for _, arg in ipairs(table.pack(...)) do
table.insert(args, arg)
end
table.insert(args, 1, true)
args.n = #args
local thread, reason = process.load(shell.resolve(program, "lua"), env, nil, command)
if not thread then
return false, reason
end
local result = nil
-- Emulate CC behavior by making yields a filtered event.pull()
while args[1] and coroutine.status(thread) ~= "dead" do
result = table.pack(coroutine.resume(thread, table.unpack(args, 2, args.n)))
if coroutine.status(thread) ~= "dead" then
if type(result[2]) == "string" then
args = table.pack(pcall(event.pull, table.unpack(result, 2, result.n)))
else
args = {true, n=1}
end
end
end
if not args[1] then
return false, args[2]
end
if not result[1] and type(result[2]) == "table" and result[2].reason == "terminated" then
if result[2].code then
return true
else
return false, "terminated"
end
end
return table.unpack(result, 1, result.n)
end
local args, options = shell.parse(...)
local history = {}
if #args == 0 and (io.input() == io.stdin or options.i) and not options.c 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
while term.isAvailable() do
local foreground = component.gpu.setForeground(0xFF0000)
term.write(expand(os.getenv("PS1") or "$ "))
component.gpu.setForeground(foreground)
local command = term.read(history)
if not command then
term.write("exit\n")
return -- eof
end
while #history > 10 do
table.remove(history, 1)
end
command = text.trim(command)
if command == "exit" then
return
elseif command ~= "" then
local result, reason = execute(command)
if not result then
io.stderr:write(reason .. "\n")
elseif term.getCursor() > 1 then
term.write("\n")
end
end
end
end
else
-- execute command.
local result = table.pack(execute(table.unpack(args)))
if not result[1] then
error(result[2])
end
return table.unpack(result, 2)
end

View File

@ -11,5 +11,5 @@ if not result then
io.stderr:write("no such alias")
else
shell.setAlias(args[1], nil)
io.write("alias removed: ", args[1], " -> ", result)
io.write("alias removed: " .. args[1] .. " -> " .. result)
end

View File

@ -14,8 +14,8 @@ for i = 1, #args do
result, reason = shell.resolve(args[i], "lua")
end
if result then
io.write(result, "\n")
io.write(result .. "\n")
else
io.stderr:write(args[i], ": ", reason, "\n")
io.stderr:write(args[i] .. ": " .. reason .. "\n")
end
end

View File

@ -1,17 +1,20 @@
local component = require("component")
local computer = require("computer")
local event = require("event")
for c, t in component.list() do
computer.pushSignal("component_added", c, t)
end
os.sleep(0.5) -- Allow signal processing by libraries.
require("term").clear()
computer.pushSignal("init") -- so libs know components are initialized.
while true do
local result, reason = os.execute(os.getenv("SHELL"))
require("term").clear()
io.write(_OSVERSION .. " (" .. math.floor(computer.totalMemory() / 1024) .. "k RAM)\n")
local result, reason = os.execute(os.getenv("SHELL") .. " -")
if not result then
print(reason)
io.stderr:write((reason or "unknown error") .. "\n")
print("Press any key to continue.")
event.pull("key")
end
end

View File

@ -10,7 +10,8 @@ function buffer.new(mode, stream)
bufferRead = "",
bufferWrite = "",
bufferSize = math.max(512, math.min(8 * 1024, computer.freeMemory() / 8)),
bufferMode = "full"
bufferMode = "full",
readTimeout = math.huge
}
mode = mode or "r"
for i = 1, unicode.len(mode) do
@ -58,7 +59,12 @@ function buffer:lines(...)
end
function buffer:read(...)
local timeout = computer.uptime() + self.readTimeout
local function readChunk()
if computer.uptime() > timeout then
error("timeout")
end
local result, reason = self.stream:read(self.bufferSize)
if result then
self.bufferRead = self.bufferRead .. result
@ -213,6 +219,14 @@ function buffer:setvbuf(mode, size)
return self.bufferMode, self.bufferSize
end
function buffer:getTimeout()
return self.readTimeout
end
function buffer:setTimeout(value)
self.readTimeout = tonumber(value)
end
function buffer:write(...)
if self.closed then
return nil, "bad file descriptor"

View File

@ -1,7 +1,8 @@
local event = require("event")
local fs = require("filesystem")
local unicode = require("unicode")
local process = require("process")
local text = require("text")
local unicode = require("unicode")
local shell = {}
local aliases = {}
@ -130,39 +131,11 @@ function shell.resolve(path, ext)
end
function shell.execute(command, env, ...)
checkArg(1, command, "string")
local parts, reason = text.tokenize(command)
if not parts then
local sh, reason = loadfile(shell.resolve(os.getenv("SHELL"), "lua"), "t", env)
if not sh then
return false, reason
end
if #parts == 0 then
return true
end
local program, args = shell.resolveAlias(parts[1], table.pack(select(2, table.unpack(parts))))
table.insert(args, 1, true)
for _, arg in ipairs(table.pack(...)) do
table.insert(args, arg)
end
args.n = #args
local thread, reason = shell.load(program, env, nil, command)
if not thread then
return false, reason
end
local result = nil
-- Emulate CC behavior by making yields a filtered event.pull()
while args[1] and coroutine.status(thread) ~= "dead" do
result = table.pack(coroutine.resume(thread, table.unpack(args, 2, args.n)))
if coroutine.status(thread) ~= "dead" then
if type(result[2]) == "string" then
args = table.pack(pcall(event.pull, table.unpack(result, 2, result.n)))
else
args = {true, n=1}
end
end
end
if not args[1] then
return false, args[2]
end
local result = table.pack(pcall(sh, command, ...))
if not result[1] and type(result[2]) == "table" and result[2].reason == "terminated" then
if result[2].code then
return true
@ -173,21 +146,15 @@ function shell.execute(command, env, ...)
return table.unpack(result, 1, result.n)
end
function shell.load(path, env, init, name)
local path, reason = shell.resolve(path, "lua")
if not path then
return nil, reason
end
return require("process").load(path, env, init, name)
end
function shell.parse(...)
local params = table.pack(...)
local args = {}
local options = {}
for i = 1, params.n do
local param = params[i]
if unicode.sub(param, 1, 1) == "-" then
if type(param) == "string" and unicode.sub(param, 1, 2) == "--" then
options[unicode.sub(param, 3)] = true
elseif type(param) == "string" and unicode.sub(param, 1, 1) == "-" then
for j = 2, unicode.len(param) do
options[unicode.sub(param, j, j)] = true
end

View File

@ -340,12 +340,14 @@ function term.write(value, wrap)
term.setCursorBlink(false)
local line, nl = value
repeat
local wrapAfter, margin = math.huge, math.huge
if wrap then
line, value, nl = text.wrap(value, w - (cursorX - 1), w)
wrapAfter, margin = w - (cursorX - 1), w
end
line, value, nl = text.wrap(value, wrapAfter, margin)
component.gpu.set(cursorX, cursorY, line)
cursorX = cursorX + unicode.len(line)
if nl or cursorX > w then
if nl or (cursorX > w and wrap) then
cursorX = 1
cursorY = cursorY + 1
end

View File

@ -10,7 +10,8 @@ function text.detab(value, tabWidth)
local spaces = tabWidth - match:len() % tabWidth
return match .. string.rep(" ", spaces)
end
return value:gsub("([^\n]-)\t", rep)
local result = value:gsub("([^\n]-)\t", rep) -- truncate results
return result
end
function text.padRight(value, length)
@ -43,6 +44,7 @@ end
function text.wrap(value, width, maxWidth)
checkArg(1, value, "string")
checkArg(2, width, "number")
checkArg(3, maxWidth, "number")
local line, nl = value:match("([^\r\n]*)([\r\n]?)") -- read until newline
if unicode.len(line) > width then -- do we even need to wrap?
local partial = unicode.sub(line, 1, width)