From c7bd5fa05e1f44b152a7e397cdd4aa3d9d429fff Mon Sep 17 00:00:00 2001 From: Wobbo Date: Thu, 20 Feb 2014 22:16:39 +0100 Subject: [PATCH 1/7] Added positional parameters --- .../assets/opencomputers/lua/rom/bin/set.lua | 11 ++++++++--- .../assets/opencomputers/lua/rom/boot/02_os.lua | 15 +++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/set.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/set.lua index f77ae96b1..6ca2272b1 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/set.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/set.lua @@ -5,14 +5,19 @@ if #args < 1 then print(k..'='..v) end else - local count = 1 + local count = 0 for _, expr in ipairs(args) do local k, v = string.match(expr, "(.-)=(.*)") if v then os.setenv(k, v) else - os.setenv(tostring(count), k) + if count == 0 then + for i = 1, os.getenv('#') do + os.setenv(i, nil) + end + end count = count + 1 + os.setenv(count, expr) end end -end \ No newline at end of file +end diff --git a/src/main/resources/assets/opencomputers/lua/rom/boot/02_os.lua b/src/main/resources/assets/opencomputers/lua/rom/boot/02_os.lua index 96147a022..f458b6444 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/boot/02_os.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/boot/02_os.lua @@ -25,7 +25,9 @@ function os.exit(code) end function os.getenv(varname) - if varname ~= nil then + if varname == '#' then + return #env + elseif varname ~= nil then return env[varname] else return env @@ -33,9 +35,14 @@ function os.getenv(varname) end function os.setenv(varname, value) - checkArg(1, varname, "string") - env[varname] = value - return env[varname] + checkArg(1, varname, "string", "number") + local success, val = pcall(tostring, value) + if success then + env[varname] = val + return env[varname] + else + return nil, val + end end function os.remove(...) From e950336d144c11912c0ca6770793fa21015f4bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Mellema?= Date: Sat, 22 Feb 2014 15:32:26 +0100 Subject: [PATCH 2/7] Made unsetting of variables possible again Before, unsetting a variable would result in "nil" in the environment :/ --- src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua index f458b6444..c98c4dac3 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua @@ -36,6 +36,7 @@ end function os.setenv(varname, value) checkArg(1, varname, "string", "number") + if value == nil then env[varname] = nil local success, val = pcall(tostring, value) if success then env[varname] = val From 95bca5ced5649a1341e18d496bea0700b8983119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sat, 22 Feb 2014 17:53:22 +0100 Subject: [PATCH 3/7] mostly memory consumption optimization of built-in stuff: moved serialization code to extra module so they only get loaded when necessary, drastically reduced memory consumption of component proxies, moved advanced shell stuff (redirects, pipes, variable expansion) to an extra program (besh.lua) to reduce memory footprint of shell module; shell uses $PS1 for the prompt now --- .../li/cil/oc/common/tileentity/Robot.scala | 2 +- .../assets/opencomputers/lua/kernel.lua | 33 +- .../assets/opencomputers/lua/rom/bin/besh.lua | 351 +++++++++++++++++ .../assets/opencomputers/lua/rom/bin/lua.lua | 2 +- .../assets/opencomputers/lua/rom/bin/sh.lua | 2 +- .../opencomputers/lua/rom/boot/01_os.lua | 1 + .../lua/rom/lib/serialization.lua | 119 ++++++ .../opencomputers/lua/rom/lib/shell.lua | 359 ++---------------- .../assets/opencomputers/lua/rom/lib/text.lua | 120 +----- src/main/resources/mcmod.info | 2 +- 10 files changed, 538 insertions(+), 453 deletions(-) create mode 100644 src/main/resources/assets/opencomputers/lua/rom/bin/besh.lua create mode 100644 src/main/resources/assets/opencomputers/lua/rom/lib/serialization.lua diff --git a/src/main/java/li/cil/oc/common/tileentity/Robot.scala b/src/main/java/li/cil/oc/common/tileentity/Robot.scala index 7efc967eb..054811d5b 100644 --- a/src/main/java/li/cil/oc/common/tileentity/Robot.scala +++ b/src/main/java/li/cil/oc/common/tileentity/Robot.scala @@ -489,7 +489,7 @@ class Robot(isRemote: Boolean) extends Computer(isRemote) with ISidedInventory w // ----------------------------------------------------------------------- // - override def installedMemory = 128 * 1024 + override def installedMemory = 96 * 1024 override def tier = 0 diff --git a/src/main/resources/assets/opencomputers/lua/kernel.lua b/src/main/resources/assets/opencomputers/lua/kernel.lua index 4e0e6dd37..baec22ae5 100644 --- a/src/main/resources/assets/opencomputers/lua/kernel.lua +++ b/src/main/resources/assets/opencomputers/lua/kernel.lua @@ -270,6 +270,17 @@ sandbox._G = sandbox -- Start of non-standard stuff. local libcomponent + +local callback = { + __call = function(method, ...) + return libcomponent.invoke(method.address, method.name, ...) + end, + __tostring = function(method) + return libcomponent.doc(method.address, method.name) or "function" + end, + __metatable = "callback" +} + libcomponent = { doc = function(address, method) checkArg(1, address, "string") @@ -283,7 +294,16 @@ libcomponent = { invoke = function(address, method, ...) checkArg(1, address, "string") checkArg(2, method, "string") - return invoke(false, address, method, ...) + local methods, reason = component.methods(address) + if not methods then + return nil, reason + end + for name, direct in pairs(methods) do + if name == method then + return invoke(direct, address, method, ...) + end + end + error("no such method", 1) end, list = function(filter) checkArg(1, filter, "string", "nil") @@ -307,15 +327,8 @@ libcomponent = { if not methods then return nil, reason end - for method, direct in pairs(methods) do - proxy[method] = setmetatable({}, { - __call = function(_, ...) - return invoke(direct, address, method, ...) - end, - __tostring = function() - return libcomponent.doc(address, method) or "function" - end - }) + for method in pairs(methods) do + proxy[method] = setmetatable({address=address,name=method}, callback) end return proxy end, diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/besh.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/besh.lua new file mode 100644 index 000000000..2fbb109dc --- /dev/null +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/besh.lua @@ -0,0 +1,351 @@ +-- BEtter SHell, a wrapper for the normal shell that adds many POSIX +-- features, such as pipes, redirects and variable expansion. + +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 function expandVars(token) + local name = nil + local special = false + local ignore = false + local ignoreChar ='' + local escaped = false + local lastEnd = 1 + local doubleQuote = false + local singleQuote = false + local endToken = {} + for i = 1, unicode.len(token) do + local char = unicode.sub(token, i, i) + if escaped then + if name then + table.insert(name, char) + end + escaped = false + elseif char == '\\' then + escaped = not escaped + table.insert(endToken, unicode.sub(token, lastEnd, i-1)) + lastEnd = i+1 + elseif char == '"' and not singleQuote then + doubleQuote = not doubleQuote + table.insert(endToken, unicode.sub(token, lastEnd, i-1)) + lastEnd = i+1 + elseif char == "'" and not doubleQuote then + singleQuote = not singleQuote + table.insert(endToken, unicode.sub(token, lastEnd, i-1)) + lastEnd = i+1 + elseif char == "$" and not doubleQuote and not singleQuote then + if name then + ignore = true + else + name = {} + table.insert(endToken, unicode.sub(token, lastEnd, i-1)) + end + elseif char == '{' and #name == 0 then + if ignore and ignoreChar == '' then + ignoreChar = '}' + else + special = true + end + elseif char == '(' and ignoreChar == '' then + ignoreChar = ')' + elseif char == '`' and special then + ignore = true + ignoreChar = '`' + elseif char == '}' and not ignore and not doubleQuote and not singleQuote then + table.insert(endToken, os.getenv(table.concat(name))) + name = nil + lastEnd = i+1 + elseif char == '"' and not singleQuote then + doubleQuote = not doubleQuote + elseif char == "'" and not doubleQuote then + singleQuote = not singleQuote + elseif name and (char:match("[%a%d_]") or special) then + if char:match("%d") and #name == 0 then + error "Identifiers can't start with a digit!" + end + table.insert(name, char) + elseif char == ignoreChar and ignore then + ignore = false + ignoreChar = '' + elseif name then -- We are done with gathering the name + table.insert(endToken, os.getenv(table.concat(name))) + name = nil + lastEnd = i + end + end + if name then + table.insert(endToken, os.getenv(table.concat(name))) + name = nil + else + table.insert(endToken, unicode.sub(token, lastEnd, -1)) + end + return table.concat(endToken) +end + +local function parseCommand(tokens) + if #tokens == 0 then + return + end + + -- Variable expansion for all command parts. + for i = 1, #tokens do + tokens[i] = expandVars(tokens[i]) + end + + -- Resolve alias for command. + local program, args = shell.resolveAlias(tokens[1], table.pack(select(2, table.unpack(tokens)))) + + -- 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, args, input, output, mode +end + +local function parseCommands(command) + local tokens, reason = text.tokenize(command) + if not tokens then + return nil, reason + end + + local commands, command = {}, {} + for i = 1, #tokens do + if tokens[i] == "|" then + if #command == 0 then + return nil, "parse error near '|'" + end + table.insert(commands, command) + command = {} + else + table.insert(command, tokens[i]) + end + end + if #command > 0 then + table.insert(commands, command) + end + + for i = 1, #commands do + commands[i] = table.pack(parseCommand(commands[i])) + if commands[i][1] == nil then + return nil, commands[i][2] + end + end + + return commands +end + +------------------------------------------------------------------------------- + +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) + end + return nil -- eof + 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) + local ok + if self.redirect.write then + ok = 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))) + ok = true + end + if ok then + return true + end + return nil, "stream is closed" +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 execute(command, env, ...) + checkArg(1, command, "string") + local commands, reason = parseCommands(command) + if not commands then + return false, reason + end + if #commands == 0 then + return true + end + + -- 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, #commands do + local program, args, input, output, mode = table.unpack(commands[i]) + local reason + threads[i], reason = shell.load(program, env, function() + if input then + local file, reason = io.open(shell.resolve(input)) + if not file then + error(reason) + 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 output then + local file, reason = io.open(shell.resolve(output), mode == "append" and "a" or "w") + if not file then + error(reason) + end + if 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 + end, command) + if not threads[i] then + return false, reason + end + + if i < #commands then + pipes[i] = require("buffer").new("rw", memoryStream.new()) + pipes[i]:setvbuf("no") + end + if i > 1 then + pipes[i - 1].stream.next = threads[i] + pipes[i - 1].stream.args = args + end + end + + local args = select(2, table.unpack(commands[1])) + table.insert(args, 1, true) + for _, arg in ipairs(table.pack(...)) do + table.insert(args, arg) + end + args.n = #args + local result = nil + for i = 1, #threads do + -- 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 + 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 pipes[i] then + pipes[i]:close() + end + if i < #threads and not result[1] then + io.write(result[2]) + 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 + 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 env = setmetatable({os=setmetatable({execute=execute}, {__index=os})}, {__index=_ENV}) +shell.execute("/bin/sh", env, ...) \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/lua.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/lua.lua index 60d95eba7..dbc6f368d 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/lua.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/lua.lua @@ -41,7 +41,7 @@ while term.isAvailable() do code, reason = load(command, "=stdin", "t", env) end if code then - local result = table.pack(pcall(code)) + local result = table.pack(xpcall(code, debug.traceback)) if not result[1] then if type(result[2]) == "table" and result[2].reason == "terminated" then os.exit(result[2].code) diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/sh.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/sh.lua index b60c13183..f5547cfc9 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/sh.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/sh.lua @@ -25,7 +25,7 @@ while true do end while term.isAvailable() do local foreground = component.gpu.setForeground(0xFF0000) - term.write("# ") + term.write(os.getenv("PS1") or "# ") component.gpu.setForeground(foreground) local command = term.read(history) if not command then diff --git a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua index 96147a022..50dbbe8d3 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua @@ -8,6 +8,7 @@ local env = { HOME="/home", MANPATH="/usr/man", PATH="/bin:/usr/bin:/home/bin:.", + PS1="# ", PWD="/", SHELL="/bin/sh", TMP="/tmp" diff --git a/src/main/resources/assets/opencomputers/lua/rom/lib/serialization.lua b/src/main/resources/assets/opencomputers/lua/rom/lib/serialization.lua new file mode 100644 index 000000000..8d22548a7 --- /dev/null +++ b/src/main/resources/assets/opencomputers/lua/rom/lib/serialization.lua @@ -0,0 +1,119 @@ +local serialization = {} + +-- Important: pretty formatting will allow presenting non-serializable values +-- but may generate output that cannot be unserialized back. +function serialization.serialize(value, pretty) + local kw = {["and"]=true, ["break"]=true, ["do"]=true, ["else"]=true, + ["elseif"]=true, ["end"]=true, ["false"]=true, ["for"]=true, + ["function"]=true, ["goto"]=true, ["if"]=true, ["in"]=true, + ["local"]=true, ["nil"]=true, ["not"]=true, ["or"]=true, + ["repeat"]=true, ["return"]=true, ["then"]=true, ["true"]=true, + ["until"]=true, ["while"]=true} + local id = "^[%a_][%w_]*$" + local ts = {} + local function s(v, l) + local t = type(v) + if t == "nil" then + return "nil" + elseif t == "boolean" then + return v and "true" or "false" + elseif t == "number" then + if v ~= v then + return "0/0" + elseif v == math.huge then + return "math.huge" + elseif v == -math.huge then + return "-math.huge" + else + return tostring(v) + end + elseif t == "string" then + return string.format("%q", v) + elseif t == "table" and pretty and getmetatable(v) and getmetatable(v).__tostring then + return tostring(v) + elseif t == "table" then + if ts[v] then + if pretty then + return "recursion" + else + error("tables with cycles are not supported") + end + end + ts[v] = true + local i, r = 1, nil + local f + if pretty then + local ks = {} + for k in pairs(v) do table.insert(ks, k) end + table.sort(ks) + local n = 0 + f = table.pack(function() + n = n + 1 + local k = ks[n] + if k ~= nil then + return k, v[k] + else + return nil + end + end) + else + f = table.pack(pairs(v)) + end + for k, v in table.unpack(f) do + if r then + r = r .. "," .. (pretty and ("\n" .. string.rep(" ", l)) or "") + else + r = "{" + end + local tk = type(k) + if tk == "number" and k == i then + i = i + 1 + r = r .. s(v, l + 1) + else + if tk == "string" and not kw[k] and string.match(k, id) then + r = r .. k + else + r = r .. "[" .. s(k, l + 1) .. "]" + end + r = r .. "=" .. s(v, l + 1) + end + end + ts[v] = nil -- allow writing same table more than once + return (r or "{") .. "}" + else + if pretty then + return tostring(t) + else + error("unsupported type: " .. t) + end + end + end + local result = s(value, 1) + local limit = type(pretty) == "number" and pretty or 10 + if pretty then + local truncate = 0 + while limit > 0 and truncate do + truncate = string.find(result, "\n", truncate + 1, true) + limit = limit - 1 + end + if truncate then + return result:sub(1, truncate) .. "..." + end + end + return result +end + +function serialization.unserialize(data) + checkArg(1, data, "string") + local result, reason = load("return " .. data, "=data", _, {math={huge=math.huge}}) + if not result then + return nil, reason + end + local ok, output = pcall(result) + if not ok then + return nil, output + end + return output +end + +return serialization \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lua/rom/lib/shell.lua b/src/main/resources/assets/opencomputers/lua/rom/lib/shell.lua index 901ca2e03..d21026ba5 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/lib/shell.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/lib/shell.lua @@ -53,244 +53,6 @@ local function findFile(name, ext) return false end -function expandVars(token) - local name = nil - local special = false - local ignore = false - local ignoreChar ='' - local escaped = false - local lastEnd = 1 - local doubleQuote = false - local singleQuote = false - local endToken = {} - for i = 1, unicode.len(token) do - local char = unicode.sub(token, i, i) - if escaped then - if name then - table.insert(name, char) - end - escaped = false - elseif char == '\\' then - escaped = not escaped - table.insert(endToken, unicode.sub(token, lastEnd, i-1)) - lastEnd = i+1 - elseif char == '"' and not singleQuote then - doubleQuote = not doubleQuote - table.insert(endToken, unicode.sub(token, lastEnd, i-1)) - lastEnd = i+1 - elseif char == "'" and not doubleQuote then - singleQuote = not singleQuote - table.insert(endToken, unicode.sub(token, lastEnd, i-1)) - lastEnd = i+1 - elseif char == "$" and not doubleQuote and not singleQuote then - if name then - ignore = true - else - name = {} - table.insert(endToken, unicode.sub(token, lastEnd, i-1)) - end - elseif char == '{' and #name == 0 then - if ignore and ignoreChar == '' then - ignoreChar = '}' - else - special = true - end - elseif char == '(' and ignoreChar == '' then - ignoreChar = ')' - elseif char == '`' and special then - ignore = true - ignoreChar = '`' - elseif char == '}' and not ignore and not doubleQuote and not singleQuote then - table.insert(endToken, os.getenv(table.concat(name))) - name = nil - lastEnd = i+1 - elseif char == '"' and not singleQuote then - doubleQuote = not doubleQuote - elseif char == "'" and not doubleQuote then - singleQuote = not singleQuote - elseif name and (char:match("[%a%d_]") or special) then - if char:match("%d") and #name == 0 then - error "Identifiers can't start with a digit!" - end - table.insert(name, char) - elseif char == ignoreChar and ignore then - ignore = false - ignoreChar = '' - elseif name then -- We are done with gathering the name - table.insert(endToken, os.getenv(table.concat(name))) - name = nil - lastEnd = i - end - end - if name then - table.insert(endToken, os.getenv(table.concat(name))) - name = nil - else - table.insert(endToken, unicode.sub(token, lastEnd, -1)) - end - return table.concat(endToken) -end - -local function resolveAlias(tokens) - local program, lastProgram = tokens[1], nil - table.remove(tokens, 1) - while true do - local alias = text.tokenize(shell.getAlias(program) or program) - program = alias[1] - if program == lastProgram then - break - end - lastProgram = program - table.remove(alias, 1) - for i = 1, #tokens do - table.insert(alias, tokens[i]) - end - tokens = alias - end - return program, tokens -end - -local function parseCommand(tokens) - if #tokens == 0 then - return - end - - -- Variable expansion for all command parts. - for i = 1, #tokens do - tokens[i] = expandVars(tokens[i]) - end - - -- Resolve alias for command. - local program, args = resolveAlias(tokens) - - -- 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, args, input, output, mode -end - -local function parseCommands(command) - local tokens, reason = text.tokenize(command) - if not tokens then - return nil, reason - end - - local commands, command = {}, {} - for i = 1, #tokens do - if tokens[i] == "|" then - if #command == 0 then - return nil, "parse error near '|'" - end - table.insert(commands, command) - command = {} - else - table.insert(command, tokens[i]) - end - end - if #command > 0 then - table.insert(commands, command) - end - - for i = 1, #commands do - commands[i] = table.pack(parseCommand(commands[i])) - if commands[i][1] == nil then - return nil, commands[i][2] - end - end - - return commands -end - -------------------------------------------------------------------------------- - -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) - end - return nil -- eof - 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) - local ok - if self.redirect.write then - ok = 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))) - ok = true - end - if ok then - return true - end - return nil, "stream is closed" -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 - ------------------------------------------------------------------------------- function shell.getAlias(alias) @@ -307,6 +69,24 @@ function shell.aliases() return pairs(aliases) end +function shell.resolveAlias(command, args) + checkArg(1, command, "string") + checkArg(2, args, "table", "nil") + local program, lastProgram = command, nil + while true do + local tokens = text.tokenize(shell.getAlias(program) or program) + program = tokens[1] + if program == lastProgram then + break + end + lastProgram = program + for i = #tokens, 2, -1 do + table.insert(args, 1, tokens[i]) + end + end + return program, args +end + function shell.getWorkingDirectory() return os.getenv("PWD") end @@ -351,105 +131,34 @@ end function shell.execute(command, env, ...) checkArg(1, command, "string") - local commands, reason = parseCommands(command) - if not commands then + local parts, reason = text.tokenize(command) + if not parts then return false, reason end - if #commands == 0 then + if #parts == 0 then return true end - - -- 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, #commands do - local program, args, input, output, mode = table.unpack(commands[i]) - local reason - threads[i], reason = shell.load(program, env, function() - if input then - local file, reason = io.open(shell.resolve(input)) - if not file then - error(reason) - 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 output then - local file, reason = io.open(shell.resolve(output), mode == "append" and "a" or "w") - if not file then - error(reason) - end - if 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 - end, command) - if not threads[i] then - return false, reason - end - - if i < #commands then - pipes[i] = require("buffer").new("rw", memoryStream.new()) - pipes[i]:setvbuf("no") - end - if i > 1 then - pipes[i - 1].stream.next = threads[i] - pipes[i - 1].stream.args = args - end - end - - local args = select(2, table.unpack(commands[1])) + 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 - for i = 1, #threads do - -- 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 - if type(result[2]) == "string" then - args = table.pack(pcall(event.pull, table.unpack(result, 2, result.n))) - else - args = {true, n=1} - end + -- 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 - if pipes[i] then - pipes[i]:close() - end - if i < #threads and not result[1] then - io.write(result[2]) - 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] diff --git a/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua b/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua index 001943021..c16c34f41 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua @@ -40,6 +40,8 @@ function text.trim(value) -- from http://lua-users.org/wiki/StringTrim return from > #value and "" or string.match(value, ".*%S", from) end +------------------------------------------------------------------------------- + function text.tokenize(value) checkArg(1, value, "string") local tokens, token = {}, "" @@ -77,122 +79,12 @@ function text.tokenize(value) return tokens end -------------------------------------------------------------------------------- - --- Important: pretty formatting will allow presenting non-serializable values --- but may generate output that cannot be unserialized back. -function text.serialize(value, pretty) - local kw = {["and"]=true, ["break"]=true, ["do"]=true, ["else"]=true, - ["elseif"]=true, ["end"]=true, ["false"]=true, ["for"]=true, - ["function"]=true, ["goto"]=true, ["if"]=true, ["in"]=true, - ["local"]=true, ["nil"]=true, ["not"]=true, ["or"]=true, - ["repeat"]=true, ["return"]=true, ["then"]=true, ["true"]=true, - ["until"]=true, ["while"]=true} - local id = "^[%a_][%w_]*$" - local ts = {} - local function s(v, l) - local t = type(v) - if t == "nil" then - return "nil" - elseif t == "boolean" then - return v and "true" or "false" - elseif t == "number" then - if v ~= v then - return "0/0" - elseif v == math.huge then - return "math.huge" - elseif v == -math.huge then - return "-math.huge" - else - return tostring(v) - end - elseif t == "string" then - return string.format("%q", v) - elseif t == "table" and pretty and getmetatable(v) and getmetatable(v).__tostring then - return tostring(v) - elseif t == "table" then - if ts[v] then - if pretty then - return "recursion" - else - error("tables with cycles are not supported") - end - end - ts[v] = true - local i, r = 1, nil - local f - if pretty then - local ks = {} - for k in pairs(v) do table.insert(ks, k) end - table.sort(ks) - local n = 0 - f = table.pack(function() - n = n + 1 - local k = ks[n] - if k ~= nil then - return k, v[k] - else - return nil - end - end) - else - f = table.pack(pairs(v)) - end - for k, v in table.unpack(f) do - if r then - r = r .. "," .. (pretty and ("\n" .. string.rep(" ", l)) or "") - else - r = "{" - end - local tk = type(k) - if tk == "number" and k == i then - i = i + 1 - r = r .. s(v, l + 1) - else - if tk == "string" and not kw[k] and string.match(k, id) then - r = r .. k - else - r = r .. "[" .. s(k, l + 1) .. "]" - end - r = r .. "=" .. s(v, l + 1) - end - end - ts[v] = nil -- allow writing same table more than once - return (r or "{") .. "}" - else - if pretty then - return tostring(t) - else - error("unsupported type: " .. t) - end - end - end - local result = s(value, 1) - local limit = type(pretty) == "number" and pretty or 10 - if pretty then - local truncate = 0 - while limit > 0 and truncate do - truncate = string.find(result, "\n", truncate + 1, true) - limit = limit - 1 - end - if truncate then - return result:sub(1, truncate) .. "..." - end - end - return result +function text.serialize(value, pretty) -- deprecated, use serialization module + return require("serialization").serialize(value, pretty) end -function text.unserialize(data) - checkArg(1, data, "string") - local result, reason = load("return " .. data, "=data", _, {math={huge=math.huge}}) - if not result then - return nil, reason - end - local ok, output = pcall(result) - if not ok then - return nil, output - end - return output +function text.unserialize(data) -- deprecated, use serialization module + return require("serialization").unserialize(data) end ------------------------------------------------------------------------------- diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 7e81db182..b193b4efa 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -4,7 +4,7 @@ "modid": "OpenComputers", "name": "OpenComputers", "description": "This mod adds modular computers and robots that can be programmed in Lua.", - "version": "1.2.0", + "version": "1.2.1", "mcversion": "1.6.4", "url": "https://github.com/MightyPirates/OpenComputers/wiki", "authors": ["Florian 'Sangar' Nücke", "Johannes 'Lord Joda' Lohrer", "Everyone who contributed to the mod on Github - thank you!"], From 65a1c58bcfa0446eeb1b88efb187598f6e53879e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sat, 22 Feb 2014 17:56:42 +0100 Subject: [PATCH 4/7] fixed os.setenv --- .../assets/opencomputers/lua/rom/boot/01_os.lua | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua index 0d2eb25d5..8f89eb9b4 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua @@ -37,13 +37,16 @@ end function os.setenv(varname, value) checkArg(1, varname, "string", "number") - if value == nil then env[varname] = nil - local success, val = pcall(tostring, value) - if success then - env[varname] = val - return env[varname] + if value == nil then + env[varname] = nil else - return nil, val + local success, val = pcall(tostring, value) + if success then + env[varname] = val + return env[varname] + else + return nil, val + end end end From e1c80cbdc8f9704fc172299c24a4c331e53c848c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sat, 22 Feb 2014 21:32:42 +0100 Subject: [PATCH 5/7] make man also look for files ending with .man --- src/main/resources/assets/opencomputers/lua/rom/bin/man.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua index 8978409c8..d452dac5b 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua @@ -10,7 +10,7 @@ end local topic = args[1] for path in string.gmatch(os.getenv("MANPATH"), "[^:]+") do - path = fs.concat(path, topic) + path = shell.resolve(fs.concat(path, topic), "man") if fs.exists(path) and not fs.isDirectory(path) then os.execute("more " .. path) os.exit() From 7ef0d3efbee4565c347b800ddd9c7026b55ad54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 23 Feb 2014 01:23:43 +0100 Subject: [PATCH 6/7] added PAGER env var and using it in man --- src/main/resources/assets/opencomputers/lua/rom/bin/ls.lua | 5 ++++- src/main/resources/assets/opencomputers/lua/rom/bin/man.lua | 2 +- .../resources/assets/opencomputers/lua/rom/boot/01_os.lua | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/ls.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/ls.lua index ca96e704e..5be963a2c 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/ls.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/ls.lua @@ -7,6 +7,7 @@ if #dirs == 0 then table.insert(dirs, ".") end +io.output():setvbuf("line") for i = 1, #dirs do local path = shell.resolve(dirs[i]) if #dirs > 1 then @@ -21,6 +22,7 @@ for i = 1, #dirs do else local function setColor(c) if component.gpu.getForeground() ~= c then + io.stdout:flush() component.gpu.setForeground(c) end end @@ -67,4 +69,5 @@ for i = 1, #dirs do io.write("\n") end end -end \ No newline at end of file +end +io.output():setvbuf("no") \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua index d452dac5b..88ac41518 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/man.lua @@ -12,7 +12,7 @@ local topic = args[1] for path in string.gmatch(os.getenv("MANPATH"), "[^:]+") do path = shell.resolve(fs.concat(path, topic), "man") if fs.exists(path) and not fs.isDirectory(path) then - os.execute("more " .. path) + os.execute(os.getenv("PAGER") .. " " .. path) os.exit() end end diff --git a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua index 8f89eb9b4..3124dd635 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/boot/01_os.lua @@ -7,6 +7,7 @@ local unicode = require("unicode") local env = { HOME="/home", MANPATH="/usr/man", + PAGER="/bin/more", PATH="/bin:/usr/bin:/home/bin:.", PS1="# ", PWD="/", From a6cc344fccca11f6dbc943fd2ea94523b44c19fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 23 Feb 2014 01:29:38 +0100 Subject: [PATCH 7/7] moved text wrapping to utility method in text module; properly wrapping text in more now; fixed rendering bug in term.read --- .../assets/opencomputers/lua/rom/bin/more.lua | 15 +++---- .../assets/opencomputers/lua/rom/lib/term.lua | 44 +++++++------------ .../assets/opencomputers/lua/rom/lib/text.lua | 20 ++++++++- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/main/resources/assets/opencomputers/lua/rom/bin/more.lua b/src/main/resources/assets/opencomputers/lua/rom/bin/more.lua index c105f14f9..a1f2d2b6c 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/bin/more.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/bin/more.lua @@ -2,11 +2,12 @@ local component = require("component") local keyboard = require("keyboard") local shell = require("shell") local term = require("term") +local text = require("text") local unicode = require("unicode") local args = shell.parse(...) if #args == 0 then - io.write("Usage: less ") + io.write("Usage: more ") return end @@ -29,13 +30,9 @@ while true do return end end - if unicode.len(line) > w then - io.write(unicode.sub(line, 1, w), "\n") - line = unicode.sub(line, w + 1) - else - io.write(line, "\n") - line = nil - end + local wrapped + wrapped, line = text.wrap(text.detab(line), w, w) + io.write(wrapped .. "\n") i = i + 1 end term.setCursor(1, h) @@ -53,4 +50,4 @@ while true do end end end -end +end \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/lua/rom/lib/term.lua b/src/main/resources/assets/opencomputers/lua/rom/lib/term.lua index bd0b9b579..2298b0b2c 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/lib/term.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/lib/term.lua @@ -102,21 +102,22 @@ function term.read(history) scrollY = nby - 1 + nbx = math.max(1, math.min(unicode.len(history[nby]) + 1, nbx)) local ncx = nbx + offset - scrollX if ncx > w then local sx = nbx - (w - offset) local dx = math.abs(scrollX - sx) scrollX = sx component.gpu.copy(1 + offset + dx, cy, w - offset - dx, 1, -dx, 0) - local str = unicode.sub(line(), nbx - (dx - 1), nbx) + local str = unicode.sub(history[nby], nbx - (dx - 1), nbx) str = text.padRight(str, dx) - component.gpu.set(1 + (w - dx), cy, str) + component.gpu.set(1 + math.max(offset, w - dx), cy, unicode.sub(str, 1 + math.max(0, dx - (w - offset)))) elseif ncx < 1 + offset then local sx = nbx - 1 local dx = math.abs(scrollX - sx) scrollX = sx component.gpu.copy(1 + offset, cy, w - offset - dx, 1, dx, 0) - local str = unicode.sub(line(), nbx, nbx + dx) + local str = unicode.sub(history[nby], nbx, nbx + dx) --str = text.padRight(str, dx) component.gpu.set(1 + offset, cy, str) end @@ -172,7 +173,7 @@ function term.read(history) local function up() local cbx, cby = getCursor() if cby > 1 then - setCursor(cbx, cby - 1) + setCursor(1, cby - 1) redraw() ende() end @@ -181,7 +182,7 @@ function term.read(history) local function down() local cbx, cby = getCursor() if cby < #history then - setCursor(cbx, cby + 1) + setCursor(1, cby + 1) redraw() ende() end @@ -337,8 +338,14 @@ function term.write(value, wrap) end local blink = term.getCursorBlink() term.setCursorBlink(false) - local function checkCursor() - if cursorX > w then + local line, nl = value + repeat + if wrap then + line, value, nl = text.wrap(value, w - (cursorX - 1), w) + end + component.gpu.set(cursorX, cursorY, line) + cursorX = cursorX + unicode.len(line) + if nl or cursorX > w then cursorX = 1 cursorY = cursorY + 1 end @@ -347,28 +354,7 @@ function term.write(value, wrap) component.gpu.fill(1, h, w, 1, " ") cursorY = h end - end - for line, nl in value:gmatch("([^\r\n]*)([\r\n]?)") do - while wrap and unicode.len(line) > w - (cursorX - 1) do - local partial = unicode.sub(line, 1, w - (cursorX - 1)) - local wordWrapped = partial:match("(.*[^a-zA-Z0-9._])") - if wordWrapped or unicode.len(line) > w then - partial = wordWrapped or partial - line = unicode.sub(line, unicode.len(partial) + 1) - component.gpu.set(cursorX, cursorY, partial) - end - cursorX = math.huge - checkCursor() - end - if unicode.len(line) > 0 then - component.gpu.set(cursorX, cursorY, line) - cursorX = cursorX + unicode.len(line) - end - if unicode.len(nl) == 1 then - cursorX = math.huge - checkCursor() - end - end + until not wrap or not value term.setCursorBlink(blink) end diff --git a/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua b/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua index c16c34f41..5e17aa8b3 100644 --- a/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua +++ b/src/main/resources/assets/opencomputers/lua/rom/lib/text.lua @@ -5,7 +5,7 @@ local text = {} function text.detab(value, tabWidth) checkArg(1, value, "string") checkArg(2, tabWidth, "number", "nil") - tabWidth = tabWidth or 4 + tabWidth = tabWidth or 8 local function rep(match) local spaces = tabWidth - match:len() % tabWidth return match .. string.rep(" ", spaces) @@ -40,6 +40,24 @@ function text.trim(value) -- from http://lua-users.org/wiki/StringTrim return from > #value and "" or string.match(value, ".*%S", from) end +function text.wrap(value, width, maxWidth) + checkArg(1, value, "string") + checkArg(2, width, "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) + local wrapped = partial:match("(.*[^a-zA-Z0-9._])") + if wrapped or unicode.len(line) > maxWidth then + partial = wrapped or partial + return partial, unicode.sub(value, unicode.len(partial) + 1), true + else + return "", value, true -- write in new line. + end + end + local start = unicode.len(line) + unicode.len(nl) + 1 + return line, start <= unicode.len(value) and unicode.sub(value, start) or nil, unicode.len(nl) > 0 +end + ------------------------------------------------------------------------------- function text.tokenize(value)