From ad33ec285a15af13d880fc535be016a05c8f44f8 Mon Sep 17 00:00:00 2001 From: payonel Date: Sun, 24 Apr 2016 21:55:43 -0700 Subject: [PATCH] big awesome update for devfs, command substitution, and io redirection Added another 60 tests, now totalling 901 all-passing tests. This change also reduces memory cost for boot by about 1300 bytes. This change should be fully compatible with any 1.6 user scripts. This makes no change to api, only improves shell support and makes important process handle cleanup (when pipes and redirection is used - we are not auto-closing user file handles). --- .../opencomputers/loot/OpenOS/boot/03_io.lua | 19 +- .../loot/OpenOS/boot/10_devfs.lua | 7 + .../opencomputers/loot/OpenOS/lib/devfs.lua | 87 +++++ .../opencomputers/loot/OpenOS/lib/pipes.lua | 37 +- .../opencomputers/loot/OpenOS/lib/process.lua | 15 +- .../opencomputers/loot/OpenOS/lib/sh.lua | 369 ++++++++++-------- .../opencomputers/loot/OpenOS/lib/shell.lua | 2 +- .../opencomputers/loot/OpenOS/lib/text.lua | 18 +- 8 files changed, 345 insertions(+), 209 deletions(-) create mode 100644 src/main/resources/assets/opencomputers/loot/OpenOS/boot/10_devfs.lua create mode 100644 src/main/resources/assets/opencomputers/loot/OpenOS/lib/devfs.lua diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua index 945ae2fa0..34a58b172 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua @@ -33,14 +33,19 @@ function stdoutStream:write(str) end function stderrStream:write(str) - local component = require("component") - if component.isAvailable("gpu") and component.gpu.getDepth() and component.gpu.getDepth() > 1 then - local foreground = component.gpu.setForeground(0xFF0000) - term.write(str, true) - component.gpu.setForeground(foreground) - else - term.write(str, true) + local gpu = term.gpu() + local set_depth = gpu and gpu.getDepth() and gpu.getDepth() > 1 + + if set_depth then + set_depth = gpu.setForeground(0xFF0000) end + + term.drawText(str, true) + + if set_depth then + gpu.setForeground(set_depth) + end + return self end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/10_devfs.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/10_devfs.lua new file mode 100644 index 000000000..22f1fd779 --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/10_devfs.lua @@ -0,0 +1,7 @@ +require("filesystem").mount( +setmetatable({ + isReadOnly = function()return true end +}, +{ + __index=function(tbl,key)return require("devfs")[key]end +}), "/dev") diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/devfs.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/devfs.lua new file mode 100644 index 000000000..ae413417f --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/devfs.lua @@ -0,0 +1,87 @@ +local fs = require("filesystem") + +local proxy = {points={},address=require("guid").next()} + +local nop = function()end + +function proxy.getLabel() + return "devfs" +end + +function proxy.list() + local keys = {} + for k,v in pairs(proxy.points) do + table.insert(keys, k) + end + return keys +end + +function proxy.exists(path) + return not not proxy.points[path] +end + +function proxy.remove(path) + if not proxy.exists(path) then return false end + proxy.points[path] = nil + return true +end + +function proxy.isDirectory(path) + return false +end + +function proxy.size(path) + return 0 +end + +function proxy.lastModified(path) + return fs.lastModified("/dev/") +end + +function proxy.read(h,...) + return h:read(...) +end + +function proxy.write(h,...) + return h:write(...) +end + +proxy.close = nop + +function proxy.open(path, mode) + checkArg(1, path, "string") + + local handle = proxy.points[path] + if not handle then return nil, "device point [" .. path .. "] does not exist" end + + local msg = "device point [" .. path .. "] cannot be opened for " + + if mode == "r" then + if not handle.read then + return nil, msg .. "read" + end + else + if not handle.write then + return nil, msg .. "write" + end + end + + return handle +end + +function proxy.create(path, handle) + handle.close = handle.close or nop + proxy.points[path] = handle + return true +end + +proxy.create("null", {write = nop}) +proxy.create("random", {read = function(_,n) + local chars = {} + for i=1,n do + table.insert(chars,string.char(math.random(0,255))) + end + return table.concat(chars) +end}) + +return proxy diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/pipes.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/pipes.lua index 9231a25f1..8c1e972a8 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/pipes.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/pipes.lua @@ -15,16 +15,14 @@ function pipeStream.new(pm) return setmetatable(stream, metatable) end function pipeStream:resume() - local yield_args = table.pack(self.pm.pco.resume_all(table.unpack(self.pm.args))) + local yield_args = table.pack(self.pm.pco.resume_all()) if not yield_args[1] then - self.pm.args = {false} self.pm.dead = true if not yield_args[1] and yield_args[2] then io.stderr:write(tostring(yield_args[2]) .. "\n") end end - self.pm.args = {true} return table.unpack(yield_args) end function pipeStream:close() @@ -134,7 +132,6 @@ function plib.internal.create(fp) local pco = setmetatable( { stack = {}, - args = {}, next = nil, create = _co.create, wrap = _co.wrap, @@ -318,7 +315,7 @@ function pipeManager.new(prog, mode, env) end local pm = setmetatable( - {dead=false,closed=false,args={},prog=prog,mode=mode,env=env}, + {dead=false,closed=false,prog=prog,mode=mode,env=env}, {__index=pipeManager} ) pm.prog_id = pm.mode == "r" and 1 or 2 @@ -328,37 +325,29 @@ function pipeManager.new(prog, mode, env) function()pm.dead=true end pm.commands = {} - pm.commands[pm.prog_id] = {shellPath, sh.internal.buildCommandRedirects({})} - pm.commands[pm.self_id] = {pm.handler, sh.internal.buildCommandRedirects({})} + pm.commands[pm.prog_id] = {shellPath, {}} + pm.commands[pm.self_id] = {pm.handler, {}} pm.root = function() + local startup_args = {} + local reason - pm.threads, reason, pm.inputs, pm.outputs = - sh.internal.buildPipeStream(pm.commands, pm.env) + pm.threads, reason = sh.internal.createThreads(pm.commands, {}, pm.env) if not pm.threads then pm.dead = true - return false, reason -- 2nd return is reason, not pipes, on error :) + return false, reason end - pm.pipe = reason[1] -- an array of pipes of length 1 - local startup_args = {} + pm.pipe = process.info(pm.threads[1]).data.io[1] + process.info(pm.threads[pm.prog_id]).data.args = {pm.env,pm.prog} + -- if we are the writer, we need args to resume prog if pm.mode == "w" then - pm.pipe.stream.args = {pm.env,pm.prog,n=2} - startup_args = {true,n=1} - -- also, if we are the writer, we need to intercept the reader - pm.pipe.stream.redirect.read = plib.internal.redirectRead(pm) - else - startup_args = {true,pm.env,pm.prog,n=3} + pm.pipe.stream.redirect[0] = plib.internal.redirectRead(pm) end - return sh.internal.executePipeStream( - pm.threads, - {pm.pipe}, - pm.inputs, - pm.outputs, - startup_args) + return sh.internal.runThreads(pm.threads) end return pm diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/process.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/process.lua index b7d3b9b81..f62b8b1b2 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/process.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/process.lua @@ -81,7 +81,7 @@ function process.load(path, env, init, name) return string.format('%s:\n%s', msg or '', stack) end, ...) } - process.list[thread] = nil + process.internal.close(thread) if not result[1] then -- msg can be a custom error object local msg = result[2] @@ -100,6 +100,7 @@ function process.load(path, env, init, name) env = env, data = setmetatable( { + handles = {}, io = setmetatable({}, {__index=p and p.data and p.data.io or nil}), coroutine_handler = setmetatable({}, {__index=p and p.data and p.data.coroutine_handler or nil}), }, {__index=p and p.data or nil}), @@ -133,4 +134,16 @@ function process.info(levelOrThread) end end +--table of undocumented api subject to change and intended for internal use +process.internal = {} +--this is a future stub for a more complete method to kill a process +function process.internal.close(thread) + checkArg(1,thread,"thread") + local pdata = process.info(thread).data + for k,v in pairs(pdata.handles) do + v:close() + end + process.list[thread] = nil +end + return process diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua index fb63e7a19..758613098 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua @@ -8,13 +8,7 @@ local tx = require("transforms") local unicode = require("unicode") local sh = {} - -sh.internal = setmetatable({}, -{ - __tostring=function() - return "table of undocumented api subject to change and intended for internal use" - end -}) +sh.internal = {} -- --[[@@]] are not just comments, but custom annotations for delayload methods. -- See package.lua and the api wiki for more information @@ -133,29 +127,36 @@ function sh.internal.isIdentifier(key) end function sh.expand(value) - return value + local expanded = value :gsub("%$([_%w%?]+)", function(key) if key == "?" then return tostring(sh.getLastExitCode()) end - return os.getenv(key) or '' end) + return os.getenv(key) or '' + end) :gsub("%${(.*)}", function(key) if sh.internal.isIdentifier(key) then return sh.internal.expandKey(key) end error("${" .. key .. "}: bad substitution") end) + if expanded:find('`') then + expanded = sh.internal.parse_sub(expanded) + end + return expanded end function sh.internal.expand(word) if #word == 0 then return {} end - local result = '' for i=1,#word do local part = word[i] - result = result .. (not (part.qr and part.qr[3]) and sh.expand(part.txt) or part.txt) + -- sh.expand runs command substitution on backticks + -- if the entire quoted area is backtick quoted, then + -- we can save some checks by adding them back in + local q = part.qr and part.qr[1] == '`' and '`' or '' + result = result .. (not (part.qr and part.qr[3]) and sh.expand(q..part.txt..q) or part.txt) end - return {result} end @@ -205,49 +206,6 @@ function sh.hintHandler(full_line, cursor) return sh.internal.hintHandlerImpl(full_line, cursor) end -function sh.internal.buildCommandRedirects(args) - local input, output, mode = nil, nil, "write" - local 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 args, input, output, mode -end - function sh.internal.parseCommand(words) checkArg(1, words, "table") if #words == 0 then @@ -263,10 +221,11 @@ function sh.internal.parseCommand(words) if not program then return nil, evaluated_words[1] .. ": " .. reason end - return program, sh.internal.buildCommandRedirects(tx.sub(evaluated_words, 2)) + evaluated_words = tx.sub(evaluated_words, 2) + return program, evaluated_words end -function sh.internal.buildPipeStream(commands, env) +function sh.internal.createThreads(commands, eargs, env) -- 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. @@ -274,109 +233,70 @@ function sh.internal.buildPipeStream(commands, env) -- 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 = {}, {}, {}, {} + local threads = {} for i = 1, #commands do - local program, args, input, output, mode = table.unpack(commands[i]) - local process_name = tostring(program) - local reason + local program, args = table.unpack(commands[i]) + local name, thread = tostring(program) local thread_env = type(program) == "string" and env or nil - threads[i], reason = process.load(program, thread_env, function() - os.setenv("_", program) - if input then - local file, reason = io.open(shell.resolve(input)) - if not file then - error("could not open '" .. 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]) + local thread, reason = process.load(program, thread_env, function() + os.setenv("_", name) + -- popen expects each process to first write an empty string + -- this is required for proper thread order + io.write('') + end, name) + + threads[i] = thread + + if thread then + -- smart check if ios should be loaded + if tx.first(args, function(token) return token == "<" or token:find(">") end) then + args, reason = sh.internal.buildCommandRedirects(thread, args) end - if output then - local file, reason = io.open(shell.resolve(output), mode == "append" and "a" or "w") - if not file then - error("could not open '" .. output .. "': " .. reason, 0) - 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 + + if not args or not thread then + for i,t in ipairs(threads) do + process.internal.close(t) end - io.write('') - end, process_name) - if not threads[i] then - return false, reason + return nil, reason end - if i < #commands then - pipes[i] = require("buffer").new("rw", sh.internal.newMemoryStream()) - pipes[i]:setvbuf("no") - end - if i > 1 then - pipes[i - 1].stream.next = threads[i] - pipes[i - 1].stream.args = args - end + process.info(thread).data.args = tx.concat(args, eargs or {}) end - return threads, pipes, inputs, outputs + + if #threads > 1 then + sh.internal.buildPipeChain(threads) + end + + return threads end -function sh.internal.executePipeStream(threads, pipes, inputs, outputs, args) +function sh.internal.runThreads(threads) local result = {} 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 - local action = result[2] - if action == nil or type(action) == "number" then - args = table.pack(pcall(event.pull, table.unpack(result, 2, result.n))) - else - args = table.pack(coroutine.yield(table.unpack(result, 2, result.n))) - end + local thread, args = threads[i] + while coroutine.status(thread) ~= "dead" do + args = args or process.info(thread).data.args + result = table.pack(coroutine.resume(thread, table.unpack(args))) + if coroutine.status(thread) ~= "dead" then + args = sh.internal.handleThreadYield(result) -- in case this was the end of the line, args is returned result = args + if table.remove(args, 1) then + break + end 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 - if result[2].code then - result[1] = true - result.n = 1 - else - result[2] = "terminated" - end - elseif type(result[2]) == "string" then - result[2] = debug.traceback(threads[i], result[2]) - end + sh.internal.handleThreadCrash(thread, result) break end end - for _, input in ipairs(inputs) do input:close() end - for _, output in ipairs(outputs) do output:close() end return table.unpack(result) end -function sh.internal.executeStatement(env, commands, eargs) - local threads, pipes, inputs, outputs = sh.internal.buildPipeStream(commands, env) - if not threads then return false, pipes end - local args = tx.concat({true,n=1},commands[1][2] or {}, eargs) - return sh.internal.executePipeStream(threads, pipes, inputs, outputs, args) -end - -function sh.internal.executePipes(pipe_parts, eargs) +function sh.internal.executePipes(pipe_parts, eargs, env) local commands = {} for i=1,#pipe_parts do commands[i] = table.pack(sh.internal.parseCommand(pipe_parts[i])) @@ -388,9 +308,14 @@ function sh.internal.executePipes(pipe_parts, eargs) return sh.internal.ec.parseCommand end end - local result = table.pack(sh.internal.executeStatement(env,commands,eargs)) - local cmd_result = result[2] - if not result[1] then + local threads, reason = sh.internal.createThreads(commands, eargs, env) + if not threads then + io.stderr:write(reason,"\n") + return false + end + local result, cmd_result = sh.internal.runThreads(threads) + + if not result then if cmd_result then if type(cmd_result) == "string" then cmd_result = cmd_result:gsub("^/lib/process%.lua:%d+: /", '/') @@ -404,7 +329,6 @@ end function sh.execute(env, command, ...) checkArg(2, command, "string") - local eargs = {...} if command:find("^%s*#") then return true, 0 end local statements, reason = sh.internal.statements(command) if not statements or statements == true then @@ -413,15 +337,105 @@ function sh.execute(env, command, ...) return true, 0 end + local eargs = {...} + -- simple if reason then - sh.internal.ec.last = sh.internal.command_result_as_code(sh.internal.executePipes(statements,eargs)) + sh.internal.ec.last = sh.internal.command_result_as_code(sh.internal.executePipes(statements, eargs, env)) return true end - return sh.internal.execute_complex(statements) + return sh.internal.execute_complex(statements, eargs, env) end +function --[[@delayloaded-start@]] sh.internal.handleThreadYield(result) + local action = result[2] + if action == nil or type(action) == "number" then + return table.pack(pcall(event.pull, table.unpack(result, 2, result.n))) + else + return table.pack(coroutine.yield(table.unpack(result, 2, result.n))) + end +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] sh.internal.handleThreadCrash(thread, result) + if type(result[2]) == "table" and result[2].reason == "terminated" then + if result[2].code then + result[1] = true + result.n = 1 + else + result[2] = "terminated" + end + elseif type(result[2]) == "string" then + result[2] = debug.traceback(thread, result[2]) + end +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] sh.internal.buildCommandRedirects(thread, args) + local data = process.info(thread).data + local tokens, ios, handles = args, data.io, data.handles + args = {} + local from_io, to_io, mode + for i = 1, #tokens do + local token = tokens[i] + if token == "<" then + from_io = 0 + mode = "r" + else + local first_index, last_index, from_io_txt, mode_txt, to_io_txt = token:find("(%d*)(>>?)(.*)") + if mode_txt then + mode = mode_txt == ">>" and "a" or "w" + from_io = from_io_txt and tonumber(from_io_txt) or 1 + if to_io_txt ~= "" then + to_io = tonumber(to_io_txt:sub(2)) + ios[from_io] = ios[to_io] + mode = nil + end + else -- just an arg + if not mode then + table.insert(args, token) + else + local file, reason = io.open(shell.resolve(token), mode) + if not file then + return nil, "could not open '" .. token .. "': " .. reason + end + table.insert(handles, file) + ios[from_io] = file + end + mode = nil + end + end + end + + return args +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] sh.internal.buildPipeChain(threads) + local prev_pipe + for i=1,#threads do + local thread = threads[i] + local data = process.info(thread).data + local pio = data.io + + local pipe + if i < #threads then + pipe = require("buffer").new("rw", sh.internal.newMemoryStream()) + pipe:setvbuf("no") + pipe.stream.redirect[1] = rawget(pio, 1) + pio[1] = pipe + table.insert(data.handles, pipe) + end + + if prev_pipe then + prev_pipe.stream.redirect[0] = rawget(pio, 0) + prev_pipe.stream.next = thread + pio[0] = prev_pipe + end + + prev_pipe = pipe + end + +end --[[@delayloaded-end@]] + function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern) local segments = text.split(glob_pattern, {"/"}, true) local hiddens = tx.select(segments,function(e)return e:match("^%%%.")==nil end) @@ -549,7 +563,7 @@ function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor return {} end local result - local prefix, partial = line:match("^(.*=)(.*)$") + local prefix, partial = line:match("^(.*[=><])(.*)$") if not prefix then prefix, partial = line:match("^(.+%s+)(.*)$") end local partialPrefix = (partial or line) local name = partialPrefix:gsub(".*/", "") @@ -583,10 +597,11 @@ function --[[@delayloaded-start@]] sh.internal.hasValidPiping(words, pipes) return true end - pipes = pipes or tx.sub(text.syntax, 2) -- first text syntax is ; which CAN be repeated + local semi_split = tx.find(text.syntax, {";"}) -- all symbols before ; in syntax CAN be repeated + pipes = pipes or tx.sub(text.syntax, semi_split + 1) - local pies = tx.select(words, function(parts, i, t) - return (#parts == 1 and tx.first(pipes, {{parts[1].txt}}) and true or false), i + local pies = tx.select(words, function(parts, i) + return #parts == 1 and #text.split(parts[1].txt, pipes, true) == 0 and true or false end) local bad_pipe @@ -718,6 +733,7 @@ function --[[@delayloaded-start@]] sh.internal.newMemoryStream() function memoryStream:close() self.closed = true + self.redirect = {} end function memoryStream:seek() @@ -728,12 +744,12 @@ function --[[@delayloaded-start@]] sh.internal.newMemoryStream() if self.closed then return nil -- eof end - if self.redirect.read then + if self.redirect[0] then -- popen could be using this code path -- if that is the case, it is important to leave stream.buffer alone - return self.redirect.read:read(n) + return self.redirect[0]:read(n) elseif self.buffer == "" then - self.args = table.pack(coroutine.yield(table.unpack(self.result))) + process.info(self.next).data.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) @@ -741,16 +757,17 @@ function --[[@delayloaded-start@]] sh.internal.newMemoryStream() end function memoryStream:write(value) - if not self.redirect.write and self.closed then + if not self.redirect[1] 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 - elseif self.redirect.write then - return self.redirect.write:write(value) + elseif self.redirect[1] then + return self.redirect[1]:write(value) elseif not self.closed then self.buffer = self.buffer .. value - self.result = table.pack(coroutine.resume(self.next, table.unpack(self.args))) + local args = process.info(self.next).data.args + self.result = table.pack(coroutine.resume(self.next, table.unpack(args))) if coroutine.status(self.next) == "dead" then self:close() end @@ -764,26 +781,52 @@ function --[[@delayloaded-start@]] sh.internal.newMemoryStream() end local stream = {closed = false, buffer = "", - redirect = {}, result = {}, args = {}} + redirect = {}, result = {}} local metatable = {__index = memoryStream, __metatable = "memorystream"} return setmetatable(stream, metatable) end --[[@delayloaded-end@]] -function --[[@delayloaded-start@]] sh.internal.execute_complex(statements) +function --[[@delayloaded-start@]] sh.internal.execute_complex(statements, eargs, env) for si=1,#statements do local s = statements[si] local chains = sh.internal.groupChains(s) - local last_code,br = sh.internal.boolean_executor(chains, function(chain, chain_index) + local last_code = sh.internal.boolean_executor(chains, function(chain, chain_index) local pipe_parts = sh.internal.splitChains(chain) - return sh.internal.executePipes(pipe_parts, - chain_index == #chains and si == #statements and eargs or {}) + local next_args = chain_index == #chains and si == #statements and eargs or {} + return sh.internal.executePipes(pipe_parts, next_args, env) end) - if br then - io.stderr:write(br,"\n") - end sh.internal.ec.last = sh.internal.command_result_as_code(last_code) end - return true, br + return true +end --[[@delayloaded-end@]] + + +function --[[@delayloaded-start@]] sh.internal.parse_sub(input) + -- cannot use gsub here becuase it is a [C] call, and io.popen needs to yield at times + local packed = {} + -- not using for i... because i can skip ahead + local i, len = 1, #input + + while i < len do + + local fi, si, capture = input:find("`([^`]*)`", i) + + if not fi then + table.insert(packed, input:sub(i)) + break + end + + local sub = io.popen(capture) + local result = sub:read("*a") + sub:close() + -- all whitespace is replaced by single spaces + -- we requote the result because tokenize will respect this as text + table.insert(packed, (text.trim(result):gsub("%s+"," "))) + + i = si+1 + end + + return table.concat(packed) end --[[@delayloaded-end@]] return sh, local_env diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/shell.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/shell.lua index 259216f4e..9813f767b 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/shell.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/shell.lua @@ -35,7 +35,7 @@ local function findFile(name, ext) dir = fs.concat(fs.concat(dir, name), "..") local name = fs.name(name) local list = fs.list(dir) - if list then + if list and name then local files = {} for file in list do files[file] = true diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua index 2c7e97d4c..9afc1c32b 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua @@ -8,14 +8,8 @@ local text = {} local local_env = {tx=tx,unicode=unicode} text.internal = {} -setmetatable(text.internal, -{ - __tostring=function() - return 'table of undocumented api subject to change and intended for internal use' - end -}) -text.syntax = {";","&&","||","|",">>",">","<"} +text.syntax = {"^%d*>>?&%d+$",";","&&","||?","^%d*>>?",">>?","<"} function --[[@delayloaded-start@]] text.detab(value, tabWidth) checkArg(1, value, "string") @@ -163,11 +157,12 @@ function text.internal.tokenize(value, quotes, delimiters) checkArg(1, value, "string") checkArg(2, quotes, "table", "nil") checkArg(3, delimiters, "table", "nil") + local custom = not not delimiters delimiters = delimiters or text.syntax local words, reason = text.internal.words(value, quotes) - local splitter = text.escapeMagic(table.concat(delimiters)) + local splitter = text.escapeMagic(custom and table.concat(delimiters) or "<>|;&") if type(words) ~= "table" or #splitter == 0 or not value:find("["..splitter.."]") then @@ -182,7 +177,7 @@ function text.internal.words(input, quotes) checkArg(1, input, "string") checkArg(2, quotes, "table", "nil") local qr = nil - quotes = quotes or {{"'","'",true},{'"','"'}} + quotes = quotes or {{"'","'",true},{'"','"'},{'`','`'}} local function append(dst, txt, qr) local size = #dst if size == 0 or dst[size].qr ~= qr then @@ -253,9 +248,6 @@ function --[[@delayloaded-start@]] text.internal.splitWords(words, delimiters) table.insert(split_words[#split_words], part) next_word = false end - local delimLookup = tx.select(delimiters, function(e,i) - return i, e - end) for wi=1,#words do local word = words[wi] next_word = true for pi=1,#word do local part = word[pi] @@ -265,7 +257,7 @@ function --[[@delayloaded-start@]] text.internal.splitWords(words, delimiters) else local part_text_splits = text.split(part.txt, delimiters) tx.foreach(part_text_splits, function(sub_txt, spi) - local delim = delimLookup[sub_txt] + local delim = #text.split(sub_txt, delimiters, true) == 0 next_word = next_word or delim add_part({txt=sub_txt,qr=qr}) next_word = delim