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