IonutParau 687cfebd00 testing version of LuaBIOS and OpenOS
people were having issues getting them to work so now we promote consistency
2025-06-28 20:41:49 +02:00

223 lines
5.9 KiB
Lua

local process = require("process")
local shell = require("shell")
local text = require("text")
local tx = require("transforms")
local sh = {}
sh.internal = {}
function sh.internal.isWordOf(w, vs)
return w and #w == 1 and not w[1].qr and tx.first(vs,{{w[1].txt}}) ~= nil
end
local isWordOf = sh.internal.isWordOf
-------------------------------------------------------------------------------
--SH API
sh.internal.ec = {}
sh.internal.ec.parseCommand = 127
sh.internal.ec.last = 0
function sh.getLastExitCode()
return sh.internal.ec.last
end
function sh.internal.command_result_as_code(ec, reason)
-- convert lua result to bash ec
local code
if ec == false then
code = 1
elseif ec == nil or ec == true then
code = 0
elseif type(ec) ~= "number" then
code = 2 -- illegal number
else
code = ec
end
if reason and code ~= 0 then io.stderr:write(reason, "\n") end
return code
end
function sh.internal.resolveActions(input, resolved)
resolved = resolved or {}
local processed = {}
local prev_was_delim = true
local words, reason = text.internal.tokenize(input)
if not words then
return nil, reason
end
while #words > 0 do
local next = table.remove(words,1)
if isWordOf(next, {";","&&","||","|"}) then
prev_was_delim = true
resolved = {}
elseif prev_was_delim then
prev_was_delim = false
-- if current is actionable, resolve, else pop until delim
if next and #next == 1 and not next[1].qr then
local key = next[1].txt
if key == "!" then
prev_was_delim = true -- special redo
elseif not resolved[key] then
resolved[key] = shell.getAlias(key)
local value = resolved[key]
if value and key ~= value then
local replacement_tokens, resolve_reason = sh.internal.resolveActions(value, resolved)
if not replacement_tokens then
return replacement_tokens, resolve_reason
end
words = tx.concat(replacement_tokens, words)
next = table.remove(words,1)
end
end
end
end
table.insert(processed, next)
end
return processed
end
-- returns true if key is a string that represents a valid command line identifier
function sh.internal.isIdentifier(key)
if type(key) ~= "string" then
return false
end
return key:match("^[%a_][%w_]*$") == key
end
-- expand (interpret) a single quoted area
-- examples: $foo or "$foo"
function sh.expand(value)
local expanded = value
:gsub("%$([_%w%?]+)", function(key)
if key == "?" then
return tostring(sh.getLastExitCode())
end
return os.getenv(key) or ''
end)
:gsub("%${(.*)}", function(key)
if sh.internal.isIdentifier(key) then
return os.getenv(key) or ''
end
io.stderr:write("${" .. key .. "}: bad substitution\n")
os.exit(1)
end)
return expanded
end
function sh.internal.createThreads(commands, env, start_args)
-- 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 = {}
for i = 1, #commands do
local command = commands[i]
local program, args, redirects = table.unpack(command)
local name = tostring(program)
local thread_env = type(program) == "string" and env or nil
local thread, reason = process.load(program or "/dev/null", thread_env, function(...)
if redirects then
sh.internal.openCommandRedirects(redirects)
end
args = tx.concat(args, start_args[i] or {}, table.pack(...))
-- popen expects each process to first write an empty string
-- this is required for proper thread order
io.write("")
return table.unpack(args, 1, args.n or #args)
end, name)
if not thread then
for _,t in ipairs(threads) do
process.internal.close(t)
end
return nil, reason
end
threads[i] = thread
end
if #threads > 1 then
require("pipe").buildPipeChain(threads)
end
return threads
end
function sh.internal.executePipes(pipe_parts, eargs, env)
local commands = {}
for _,words in ipairs(pipe_parts) do
local args = {}
local reparse
for _,word in ipairs(words) do
local value = ""
for _,part in ipairs(word) do
reparse = reparse or part.qr or part.txt:find("[%$%*%?<>]")
value = value .. part.txt
end
args[#args + 1] = value
end
local redirects
if reparse then
args, redirects = sh.internal.evaluate(words)
if not args then
return false, redirects -- in this failure case, redirects has the error message
end
end
commands[#commands + 1] = table.pack(table.remove(args, 1), args, redirects)
end
local threads, reason = sh.internal.createThreads(commands, env, {[#commands]=eargs})
if not threads then return false, reason end
return process.internal.continue(threads[1])
end
function sh.execute(env, command, ...)
checkArg(2, command, "string")
if command:find("^%s*#") then return true, 0 end
local words, reason = sh.internal.resolveActions(command)
if type(words) ~= "table" then
return words, reason
elseif #words == 0 then
return true
end
-- MUST be table.pack for non contiguous ...
local eargs = table.pack(...)
-- simple
if not command:find("[;%$&|!<>]") then
sh.internal.ec.last = sh.internal.command_result_as_code(sh.internal.executePipes({words}, eargs, env))
return sh.internal.ec.last == 0
end
return sh.internal.execute_complex(words, eargs, env)
end
function sh.hintHandler(full_line, cursor)
return sh.internal.hintHandlerImpl(full_line, cursor)
end
require("package").delay(sh, "/lib/core/full_sh.lua")
return sh