Files
HyperionOS/Src/Hyperion-bash/bin/hysh

1229 lines
41 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--:Minify:--
syscall.open("/dev/tty/1","r") --stdin (Device 0)
syscall.open("/dev/tty/1","w") --stdout (Device 1)
syscall.open("/dev/null","w") --stderr (device 2)
local success, errorMsg = xpcall(function()
local fs = require("sys.fs")
syscall.devctl(1,"clear")
syscall.devctl(1,"sfgc",1)
syscall.devctl(1,"spos",1,1)
print("HyperionOS hysh Shell")
local userhost = (syscall.getUsername() or "Unknown").."@"..(syscall.getHostname() or "Unknown")
local commandHistory = {}
local terminate = false
syscall.setEnviron("SHELL","rtbash")
syscall.setEnviron("PATH","/bin/")
local _home = syscall.getEnviron("HOME")
if _home and _home ~= "" then
local ok = pcall(syscall.chdir, _home)
if not ok then syscall.chdir("/") end
else
syscall.chdir("/")
end
local oldWD = ""
for i = 1, 16 do
syscall.devctl(1,"sbgc",i); printInline(" ")
end
print("\n")
syscall.sigcatch(function(sig)
if sig == 1 then terminate = true end
end)
local function abspath(p)
if not p or p == "" then return syscall.getcwd() end
if p:sub(1,1) ~= "/" then p = syscall.getcwd().."/"..p end
local parts = {}
for seg in p:gmatch("[^/]+") do
if seg == ".." then if #parts > 0 then table.remove(parts) end
elseif seg ~= "." then table.insert(parts, seg) end
end
return "/"..table.concat(parts, "/")
end
local function basename(p)
p = p:gsub("/$","")
return p:match("([^/]+)$") or p
end
local function dirname(p)
p = p:gsub("/$","")
local d = p:match("^(.*)/[^/]+$")
if not d then return "." end
if d == "" then return "/" end
return d
end
local function readall(fd)
local t = {}
while true do
local ok, chunk = pcall(syscall.read, fd, 65536)
if not ok or not chunk or chunk == "" then break end
t[#t+1] = chunk
end
return table.concat(t)
end
local function readfile(path)
local ok, fd = pcall(syscall.open, path, "r")
if not ok then return nil, fd end
local data = readall(fd)
pcall(syscall.close, fd)
return data
end
local function writefile(path, data)
local ok, fd = pcall(syscall.open, path, "w")
if not ok then return false, fd end
pcall(syscall.write, fd, data)
pcall(syscall.close, fd)
return true
end
local function parseargs(rawargs, flagspec, longflags)
local opts = {}
local args = {}
longflags = longflags or {}
local i = 1
while i <= #rawargs do
local v = rawargs[i]
if v == "--" then
for j = i+1, #rawargs do args[#args+1] = rawargs[j] end
break
elseif v:sub(1,2) == "--" then
local lf = v:sub(3)
if longflags[lf] ~= nil then opts[lf] = true
else opts["_unknown"] = v end
elseif v:sub(1,1) == "-" and #v > 1 then
for k = 2, #v do
local c = v:sub(k,k)
if flagspec:find(c, 1, true) then opts[c] = true
else opts["_unknown"] = "-"..c end
end
else
args[#args+1] = v
end
i = i + 1
end
return opts, args
end
local function eachline(text, fn)
local pos = 1
while pos <= #text do
local nl = text:find("\n", pos, true)
if nl then fn(text:sub(pos, nl-1)); pos = nl+1
else fn(text:sub(pos)); break end
end
end
local function splitlines(text)
local lines = {}
eachline(text, function(l) lines[#lines+1] = l end)
return lines
end
local function copyfile(src, dst)
local data, err = readfile(src)
if not data then return false, err end
local ok, err2 = writefile(dst, data)
if not ok then return false, err2 end
local ok2, stat = pcall(syscall.stat, src)
if ok2 and stat and stat.perms then pcall(syscall.chmod, dst, stat.perms) end
return true
end
local function rmtree(path)
local t = syscall.type(path)
if t == "directory" then
local ok, list = pcall(syscall.listdir, path)
if ok then
for _, e in ipairs(list) do
rmtree(path:gsub("/$","").."/"..e)
end
end
end
if t then pcall(syscall.remove, path) end
end
local function copytree(src, dst)
pcall(syscall.mkdir, dst)
local ok, list = pcall(syscall.listdir, src)
if not ok then return end
for _, e in ipairs(list) do
local s = src:gsub("/$","").."/"..e
local d = dst:gsub("/$","").."/"..e
local t = syscall.type(s)
if t == "directory" then copytree(s, d)
elseif t == "file" then copyfile(s, d)
elseif t == "symlink" then
local ok2, tgt = pcall(syscall.readlink, s)
if ok2 then pcall(syscall.remove, d); pcall(syscall.symlink, tgt, d) end
end
end
end
local builtinCmds = {}
builtinCmds.cd = function(path)
local cwd = syscall.getcwd()
local dirIn = path or ""
if dirIn == "-" then
if oldWD == "" then print("hysh: cd: no previous directory"); return end
print(oldWD); local tmp = oldWD; oldWD = cwd; syscall.chdir(tmp); return
end
local target = abspath(dirIn)
if target:sub(-1) ~= "/" then target = target.."/" end
if not fs.isDir(target) then
print("hysh: cd: "..dirIn..": No such directory"); return
end
oldWD = cwd; syscall.chdir(target)
end
builtinCmds.exit = function(code)
syscall.exit(tonumber(code) or 0)
end
builtinCmds.echo = function(...)
local args = {...}
local n = false
local start = 1
if args[1] == "-n" then n = true; start = 2 end
local out = table.concat(args, " ", start)
if n then printInline(out) else print(out) end
end
builtinCmds.pwd = function()
print(syscall.getcwd())
end
builtinCmds["true"] = function() end
builtinCmds["false"] = function() end
builtinCmds.sleep = function(...)
local args = {...}
if #args == 0 then print("sleep: missing operand"); return end
local total = 0
for _, a in ipairs(args) do
local n, u = a:match("^([%d%.]+)([smhd]?)$")
if not n then print("sleep: invalid time '"..a.."'"); return end
n = tonumber(n)
if u == "m" then n = n * 60
elseif u == "h" then n = n * 3600
elseif u == "d" then n = n * 86400 end
total = total + n
end
sleep(total)
end
builtinCmds.clear = function()
syscall.devctl(1,"clear")
syscall.devctl(1,"sfgc",1)
syscall.devctl(1,"spos",1,1)
end
builtinCmds.whoami = function()
print(syscall.getUsername() or "unknown")
end
builtinCmds.hostname = function(...)
local args = {...}
if #args == 0 then
print(syscall.getHostname() or "unknown")
else
local ok, err = pcall(syscall.setHostname, args[1])
if not ok then print("hostname: "..tostring(err)) end
end
end
builtinCmds.uname = function(...)
local opts, _ = parseargs({...}, "asnrm")
local all = opts.a
local parts = {}
local results = {}
for word in string.gmatch(syscall.version(), "%S+") do
table.insert(results, word)
end
if all or opts.s or (not opts.s and not opts.n and not opts.r and not opts.m) then
parts[#parts+1] = results[1]
end
if all or opts.n then parts[#parts+1] = (syscall.getHostname() or "hyperion") end
if all or opts.r then parts[#parts+1] = results[2] end
if all or opts.m then parts[#parts+1] = "virtual" end
print(table.concat(parts, " "))
end
builtinCmds.printenv = function(...)
local args = {...}
local vars = {"PATH","HOME","USER","SHELL","TERM","HOSTNAME","PWD","OLDPWD","LANG","TZ","EDITOR"}
if #args == 0 then
for _, k in ipairs(vars) do
local ok, v = pcall(syscall.getEnviron, k)
if ok and v then print(k.."="..v) end
end
else
for _, k in ipairs(args) do
local ok, v = pcall(syscall.getEnviron, k)
if ok and v then print(v) end
end
end
end
builtinCmds.env = function(...)
local args = {...}
local i = 1
while i <= #args and args[i]:match("^[%w_]+=") do
local k, v = args[i]:match("^([^=]+)=(.*)")
pcall(syscall.setEnviron, k, v)
i = i + 1
end
if i > #args then
builtinCmds.printenv()
end
end
builtinCmds.touch = function(...)
local _, args = parseargs({...}, "amc")
if #args == 0 then print("touch: missing operand"); return end
for _, a in ipairs(args) do
local path = abspath(a)
if not syscall.exists(path) then
local ok, fd = pcall(syscall.open, path, "w")
if ok then pcall(syscall.close, fd)
else print("touch: cannot create '"..a.."': "..tostring(fd)) end
end
end
end
builtinCmds.mkdir = function(...)
local opts, args = parseargs({...}, "p")
if #args == 0 then print("mkdir: missing operand"); return end
for _, a in ipairs(args) do
local path = abspath(a)
if path:sub(-1) ~= "/" then path = path.."/" end
if fs.isDir(path) then
print("mkdir: cannot create '"..a.."': Directory exists"); return
end
local ok, err = pcall(syscall.mkdir, path)
if not ok then print("mkdir: cannot create '"..a.."': "..tostring(err)) end
end
end
builtinCmds.rm = function(...)
local opts, args = parseargs({...}, "rRf")
if #args == 0 then print("rm: missing operand"); return end
local recursive = opts.r or opts.R
for _, a in ipairs(args) do
local path = abspath(a)
local t = syscall.type(path)
if not t then
if not opts.f then print("rm: cannot remove '"..a.."': No such file or directory") end
elseif t == "directory" then
if not recursive then print("rm: cannot remove '"..a.."': Is a directory")
else rmtree(path) end
else
local ok, err = pcall(syscall.remove, path)
if not ok then print("rm: cannot remove '"..a.."': "..tostring(err)) end
end
end
end
builtinCmds.cp = function(...)
local opts, args = parseargs({...}, "rRp")
if #args < 2 then print("cp: missing operand"); return end
local recursive = opts.r or opts.R
local dst = abspath(args[#args])
local dstIsDir = syscall.type(dst) == "directory"
if #args > 2 and not dstIsDir then
print("cp: target '"..args[#args].."' is not a directory"); return
end
for i = 1, #args-1 do
local src = abspath(args[i])
local t = syscall.type(src)
if not t then
print("cp: '"..args[i].."': No such file or directory")
elseif t == "directory" then
if not recursive then print("cp: omitting directory '"..args[i].."'")
else copytree(src, dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst) end
else
local d = dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst
local ok, err = copyfile(src, d)
if not ok then print("cp: '"..args[i].."': "..tostring(err)) end
end
end
end
builtinCmds.mv = function(...)
local opts, args = parseargs({...}, "f")
if #args < 2 then print("mv: missing operand"); return end
local dst = abspath(args[#args])
local dstIsDir = syscall.type(dst) == "directory"
if #args > 2 and not dstIsDir then
print("mv: target '"..args[#args].."' is not a directory"); return
end
for i = 1, #args-1 do
local src = abspath(args[i])
local t = syscall.type(src)
if not t then
print("mv: '"..args[i].."': No such file or directory")
else
local d = dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst
if t == "directory" then
copytree(src, d); rmtree(src)
else
local ok, err = copyfile(src, d)
if ok then pcall(syscall.remove, src)
else print("mv: '"..args[i].."': "..tostring(err)) end
end
end
end
end
builtinCmds.cat = function(...)
local args = {...}
if #args == 0 then
while true do
local ok, chunk = pcall(syscall.read, 0, 4096)
if not ok or not chunk or chunk == "" then break end
printInline(chunk)
end
print("")
return
end
for _, a in ipairs(args) do
local path = abspath(a)
if not syscall.exists(path) then
print("cat: "..a..": No such file or directory")
else
local ok, fd = pcall(syscall.open, path, "r")
if not ok then print("cat: "..a..": "..tostring(fd))
else
while true do
local ok2, chunk = pcall(syscall.read, fd, 65536)
if not ok2 or not chunk or chunk == "" then break end
printInline(chunk)
end
pcall(syscall.close, fd)
end
end
end
end
builtinCmds.head = function(...)
local raw = {...}
local n = 10
local files = {}
local i = 1
while i <= #raw do
local v = raw[i]
if v == "-n" then i=i+1; n=tonumber(raw[i]) or 10
elseif v:match("^%-n%d+$") then n=tonumber(v:sub(3))
elseif v:match("^%-%d+$") then n=tonumber(v:sub(2))
elseif v:sub(1,1) ~= "-" or v == "-" then files[#files+1] = v
end
i=i+1
end
local multi = #files > 1
local function dohead(text, label)
if multi then
syscall.devctl(1,"sfgc",4); print("==> "..label.." <=="); syscall.devctl(1,"sfgc",1)
end
local count = 0
for line in (text.."\n"):gmatch("([^\n]*)\n") do
if count >= n then break end
print(line); count = count+1
end
end
if #files == 0 then
dohead(readall(0), "stdin")
else
for _, a in ipairs(files) do
if a == "-" then dohead(readall(0), "stdin")
else
local data, err = readfile(abspath(a))
if not data then print("head: "..a..": "..tostring(err))
else dohead(data, a) end
end
end
end
end
builtinCmds.tail = function(...)
local raw = {...}
local n = 10
local files = {}
local i = 1
while i <= #raw do
local v = raw[i]
if v == "-n" then i=i+1; n=tonumber(raw[i]) or 10
elseif v:match("^%-n%d+$") then n=tonumber(v:sub(3))
elseif v:match("^%-%d+$") then n=tonumber(v:sub(2))
elseif v:sub(1,1) ~= "-" or v == "-" then files[#files+1] = v
end
i=i+1
end
local multi = #files > 1
local function dotail(text, label)
if multi then
syscall.devctl(1,"sfgc",4); print("==> "..label.." <=="); syscall.devctl(1,"sfgc",1)
end
local lines = splitlines(text)
local start = math.max(1, #lines - n + 1)
for j = start, #lines do print(lines[j]) end
end
if #files == 0 then dotail(readall(0), "stdin")
else
for _, a in ipairs(files) do
if a == "-" then dotail(readall(0), "stdin")
else
local data, err = readfile(abspath(a))
if not data then print("tail: "..a..": "..tostring(err))
else dotail(data, a) end
end
end
end
end
builtinCmds.wc = function(...)
local opts, args = parseargs({...}, "lwc")
local showAll = not opts.l and not opts.w and not opts.c
if showAll then opts.l=true; opts.w=true; opts.c=true end
local function count(text)
local l,w,b = 0,0,#text
for _ in text:gmatch("\n") do l=l+1 end
for _ in text:gmatch("%S+") do w=w+1 end
return l,w,b
end
local function fmt(l,w,c,lbl)
local p={}
if opts.l then p[#p+1]=string.format("%7d",l) end
if opts.w then p[#p+1]=string.format("%7d",w) end
if opts.c then p[#p+1]=string.format("%7d",c) end
if lbl then p[#p+1]=" "..lbl end
print(table.concat(p))
end
local tl,tw,tc = 0,0,0
if #args == 0 then
local l,w,c = count(readall(0)); fmt(l,w,c)
else
for _, a in ipairs(args) do
local data, err = readfile(abspath(a))
if not data then print("wc: "..a..": "..tostring(err))
else
local l,w,c = count(data); fmt(l,w,c,a)
tl=tl+l; tw=tw+w; tc=tc+c
end
end
if #args > 1 then fmt(tl,tw,tc,"total") end
end
end
builtinCmds.grep = function(...)
local opts, args = parseargs({...}, "ivnlcrR", {["ignore-case"]=true,["invert-match"]=true})
if #args == 0 then print("grep: missing pattern"); return end
local pat = args[1]
if opts.i or opts["ignore-case"] then pat = pat:lower() end
local recursive = opts.r or opts.R
local found = false
local function greptext(text, label, showlabel)
local count = 0
local linenum = 0
local hasMatch = false
eachline(text, function(line)
linenum = linenum+1
local test = (opts.i or opts["ignore-case"]) and line:lower() or line
local m = (test:find(pat) ~= nil)
if opts.v or opts["invert-match"] then m = not m end
if m then
count=count+1; hasMatch=true; found=true
if not opts.l and not opts.c then
local out = ""
if showlabel then out=out..label..":" end
if opts.n then out=out..linenum..":" end
print(out..line)
end
end
end)
if opts.l and hasMatch then print(label) end
if opts.c then
print((showlabel and label..":" or "")..count)
end
end
local function grepfile(path, label, showlabel)
local data, err = readfile(path)
if not data then print("grep: "..label..": "..tostring(err)); return end
greptext(data, label, showlabel)
end
local function grepdir(dir, prefix)
local ok, list = pcall(syscall.listdir, dir)
if not ok then return end
for _, e in ipairs(list) do
local fp = dir:gsub("/$","").."/"..e
local lbl = (prefix ~= "" and prefix.."/" or "")..e
local t = syscall.type(fp)
if t == "directory" then grepdir(fp, lbl)
elseif t == "file" then grepfile(fp, lbl, true) end
end
end
if #args == 1 then
greptext(readall(0), "(stdin)", false)
else
local showlabel = #args > 2 or recursive
for i = 2, #args do
local path = abspath(args[i])
local t = syscall.type(path)
if t == "directory" then
if recursive then grepdir(path, args[i])
else print("grep: "..args[i]..": Is a directory") end
elseif t then
grepfile(path, args[i], showlabel)
else
print("grep: "..args[i]..": No such file or directory")
end
end
end
end
builtinCmds.sort = function(...)
local opts, args = parseargs({...}, "rnu")
local lines = {}
local function addlines(text)
local ls = splitlines(text)
for _, l in ipairs(ls) do lines[#lines+1] = l end
end
if #args == 0 then addlines(readall(0))
else
for _, a in ipairs(args) do
local data, err = readfile(abspath(a))
if not data then print("sort: "..a..": "..tostring(err))
else addlines(data) end
end
end
table.sort(lines, function(a,b)
if opts.n then
local na = tonumber(a:match("^%-?%d+%.?%d*")) or 0
local nb = tonumber(b:match("^%-?%d+%.?%d*")) or 0
return opts.r and na > nb or na < nb
end
return opts.r and a > b or a < b
end)
local prev = nil
for _, l in ipairs(lines) do
if not opts.u or l ~= prev then print(l); prev = l end
end
end
builtinCmds.uniq = function(...)
local opts, args = parseargs({...}, "cdui")
local text
if args[1] then
local data, err = readfile(abspath(args[1]))
if not data then print("uniq: "..args[1]..": "..tostring(err)); return end
text = data
else text = readall(0) end
local prev, prevCount = nil, 0
local out = {}
local function emit(line, count)
if opts.d and count == 1 then return end
if opts.u and count > 1 then return end
out[#out+1] = opts.c and string.format("%7d %s", count, line) or line
end
eachline(text, function(line)
local cmp = opts.i and line:lower() or line
local cprev = opts.i and (prev and prev:lower()) or prev
if cmp == cprev then prevCount = prevCount+1
else
if prev ~= nil then emit(prev, prevCount) end
prev = line; prevCount = 1
end
end)
if prev ~= nil then emit(prev, prevCount) end
if args[2] then
local ok, err = writefile(abspath(args[2]), table.concat(out, "\n").."\n")
if not ok then print("uniq: "..args[2]..": "..tostring(err)) end
else
for _, l in ipairs(out) do print(l) end
end
end
builtinCmds.tee = function(...)
local opts, args = parseargs({...}, "a")
local mode = opts.a and "a" or "w"
local fds = {}
for _, a in ipairs(args) do
local ok, fd = pcall(syscall.open, abspath(a), mode)
if not ok then print("tee: "..a..": "..tostring(fd))
else fds[#fds+1] = fd end
end
while true do
local ok, chunk = pcall(syscall.read, 0, 4096)
if not ok or not chunk or chunk == "" then break end
pcall(syscall.write, 1, chunk)
for _, fd in ipairs(fds) do pcall(syscall.write, fd, chunk) end
end
for _, fd in ipairs(fds) do pcall(syscall.close, fd) end
end
builtinCmds.find = function(...)
local raw = {...}
local roots, filters = {}, {}
local maxdepth, mindepth = nil, 0
local i = 1
while i <= #raw and raw[i]:sub(1,1) ~= "-" do
roots[#roots+1] = raw[i]; i=i+1
end
if #roots == 0 then roots = {"."} end
while i <= #raw do
local tok = raw[i]
if tok == "-name" then
i=i+1
local pat = "^"..(raw[i] or ""):gsub("([%.%+%-%^%$%(%)%[%]%%])","%%%1"):gsub("%*",".*"):gsub("%?",".").. "$"
filters[#filters+1] = function(_, e, _2, _3) return e:match(pat) ~= nil end
elseif tok == "-type" then
i=i+1; local ft=raw[i] or ""
filters[#filters+1] = function(_, _, t, _2)
if ft=="f" then return t=="file"
elseif ft=="d" then return t=="directory"
elseif ft=="l" then return t=="symlink" end; return true
end
elseif tok == "-maxdepth" then i=i+1; maxdepth=tonumber(raw[i]) or 0
elseif tok == "-mindepth" then i=i+1; mindepth=tonumber(raw[i]) or 0
elseif tok == "-empty" then
filters[#filters+1] = function(path, _, t, _2)
if t=="file" then local ok,s=pcall(syscall.stat,path); return ok and s and (s.size or 0)==0
elseif t=="directory" then local ok,l=pcall(syscall.listdir,path); return ok and l and #l==0 end
return false
end
end
i=i+1
end
local function match(path, e, t, depth)
for _, f in ipairs(filters) do if not f(path,e,t,depth) then return false end end
return true
end
local function walk(path, disp, depth)
local t = syscall.type(path)
if not t then return end
local e = path:match("([^/]+)/?$") or path
if depth >= mindepth and match(path,e,t,depth) then print(disp) end
if t=="directory" and (maxdepth==nil or depth<maxdepth) then
local ok,list = pcall(syscall.listdir, path)
if ok then
table.sort(list)
for _, child in ipairs(list) do
walk(path:gsub("/$","").."/"..child, disp:gsub("/$","").."/"..child, depth+1)
end
end
end
end
local cwd = syscall.getcwd()
for _, r in ipairs(roots) do
local abs = (r == ".") and cwd or abspath(r)
walk(abs, r, 0)
end
end
builtinCmds.stat = function(...)
local _, args = parseargs({...}, "")
if #args == 0 then print("stat: missing operand"); return end
local function permstr(p)
local function b(n) return math.floor(p/(2^n))%2==1 end
return (b(5) and"r"or"-")..(b(4) and"w"or"-")..(b(9) and"x"or"-")..
(b(3) and"r"or"-")..(b(2) and"w"or"-")..(b(8) and"x"or"-")..
(b(1) and"r"or"-")..(b(0) and"w"or"-")..(b(7) and"x"or"-")
end
local function octperm(p)
local function b(n) return math.floor(p/(2^n))%2 end
local o = b(6)*4096+b(5)*256+b(4)*128+b(9)*64+b(3)*32+b(2)*16+b(8)*8+b(1)*4+b(0)*2+b(7)*1
return string.format("%04o",o)
end
for _, a in ipairs(args) do
local path = abspath(a)
if not syscall.exists(path) then
print("stat: '"..a.."': No such file or directory")
else
local t = syscall.type(path)
local ok, s = pcall(syscall.stat, path)
s = ok and s or {}
local tc = t=="directory" and "d" or (t=="symlink" and "l" or "-")
print(" File: "..a)
print(" Size: "..tostring(s.size or 0).." Type: "..(t or "?"))
print(" Owner: "..tostring(s.owner or 0).." Group: "..tostring(s.group or 0))
print("Access: ("..octperm(s.perms or 0).."/"..tc..permstr(s.perms or 0)..")")
if t == "symlink" then
local ok2, tgt = pcall(syscall.readlink, path)
if ok2 then print(" Link: "..tgt) end
end
print("")
end
end
end
builtinCmds.basename = function(...)
local args = {...}
if #args == 0 then print("basename: missing operand"); return end
local p = args[1]
while #p > 1 and p:sub(-1)=="/" do p=p:sub(1,-2) end
local b = p:match("([^/]+)$") or p
if args[2] and b:sub(-#args[2]) == args[2] then b = b:sub(1, #b-#args[2]) end
print(b)
end
builtinCmds.dirname = function(...)
local args = {...}
if #args == 0 then print("dirname: missing operand"); return end
for _, p in ipairs(args) do
while #p > 1 and p:sub(-1)=="/" do p=p:sub(1,-2) end
local d = p:match("^(.*)/[^/]+$")
if not d then print(".")
elseif d == "" then print("/")
else print(d) end
end
end
builtinCmds.df = function(...)
local opts, _ = parseargs({...}, "h")
local function hfmt(n)
local u={"K","M","G","T"}; local i=0
while n>=1024 and i<#u do n=n/1024; i=i+1 end
if i==0 then return tostring(math.floor(n)) end
if n<10 then return string.format("%.1f%s",n,u[i]) end
return math.floor(n)..u[i]
end
local function fmt(n) return opts.h and hfmt(n) or tostring(n) end
print(string.format("%-20s %10s %10s %10s %6s %-s","Filesystem","Size","Used","Avail","Use%","Mounted on"))
local mounts = {{"$","/"}, {"devfs0000","/dev/"}, {"tmpfs0000","/tmp/"}}
for _, m in ipairs(mounts) do
local id, mp = m[1], m[2]
print(string.format("%-20s %10s %10s %10s %6s %-s", id, "?", "?", "?", "?%", mp))
end
end
local function listDir(dir, prefix)
local ok, entries = pcall(syscall.listdir, dir)
if not ok or not entries then return {} end
local results = {}
for _, e in ipairs(entries) do
if prefix == "" or e:sub(1, #prefix) == prefix then
local fullpath = (dir == "/" and "/" or dir.."/")..e
local t = syscall.type(fullpath)
results[#results+1] = t == "directory" and (e.."/") or e
end
end
table.sort(results)
return results
end
local function listCommands(prefix)
local results = {}
local seen = {}
for name in pairs(builtinCmds) do
if prefix == "" or name:sub(1, #prefix) == prefix then
if not seen[name] then results[#results+1] = name; seen[name] = true end
end
end
local paths = string.split(syscall.getEnviron("PATH") or "/bin/", ":")
for _, p in ipairs(paths) do
local ok, entries = pcall(syscall.listdir, p)
if ok and entries then
for _, e in ipairs(entries) do
local fullpath = (p:sub(-1)=="/" and p or p.."/")..e
local xok = pcall(syscall.access, fullpath, "x")
if xok and (prefix == "" or e:sub(1, #prefix) == prefix) then
if not seen[e] then results[#results+1] = e; seen[e] = true end
end
end
end
end
table.sort(results)
return results
end
local function commonPrefix(list)
if #list == 0 then return "" end
local pre = list[1]
for i = 2, #list do
local s = list[i]
local j = 1
while j <= #pre and j <= #s and pre:sub(j,j) == s:sub(j,j) do j = j+1 end
pre = pre:sub(1, j-1)
if pre == "" then return "" end
end
return pre
end
local function showCompletions(completions)
syscall.write(1, "\n")
local W = 51
local maxlen = 0
for _, c in ipairs(completions) do if #c > maxlen then maxlen = #c end end
local colw = maxlen + 2
local cols = math.max(1, math.floor(W / colw))
local i = 0
for _, c in ipairs(completions) do
local padded = c .. string.rep(" ", colw - #c)
syscall.write(1, padded)
i = i + 1
if i % cols == 0 then syscall.write(1, "\n") end
end
if i % cols ~= 0 then syscall.write(1, "\n") end
end
local function parseForCompletion(input, cursorPos)
local sofar = input:sub(1, cursorPos - 1)
local tokens = {}
for tok in sofar:gmatch("%S+") do tokens[#tokens+1] = tok end
local partial = sofar:match("%S+$") or ""
local isFirst = (#tokens == 0) or (sofar:sub(-1) ~= " " and #tokens == 1)
local partialDir, partialBase
if partial:find("/") then
partialDir = partial:match("^(.*/)") or "/"
partialBase = partial:match("[^/]*$") or ""
else
partialDir = nil
partialBase = partial
end
return tokens, partial, isFirst, partialDir, partialBase
end
local tabState = { last = nil, idx = 0, list = {} }
local function doTabComplete(input, cursorPos)
local tokens, partial, isFirst, partialDir, partialBase = parseForCompletion(input, cursorPos)
local candidates
if isFirst then
if partialDir then
local dir = partialDir:sub(1,1) == "/" and partialDir or (syscall.getcwd().."/"..partialDir)
candidates = listDir(dir, partialBase)
for i, c in ipairs(candidates) do candidates[i] = partialDir..c end
else
candidates = listCommands(partialBase)
end
else
local dir, base
if partialDir then
dir = partialDir:sub(1,1) == "/" and partialDir or (syscall.getcwd().."/"..partialDir)
base = partialBase
else
dir = syscall.getcwd()
base = partialBase
end
candidates = listDir(dir, base)
if partialDir then
for i, c in ipairs(candidates) do candidates[i] = partialDir..c end
end
end
if #candidates == 0 then
return input, cursorPos, false
end
local context = input.."\0"..tostring(cursorPos)
if tabState.last ~= context then
tabState.last = context
tabState.idx = 0
tabState.list = candidates
end
if #candidates == 1 then
local completed = candidates[1]
local before = input:sub(1, cursorPos - 1 - #partial)
local after = input:sub(cursorPos)
local newInput = before .. completed .. after
local newCursor = #before + #completed + 1
tabState.last = nil
return newInput, newCursor, true
end
local pre = commonPrefix(candidates)
if #pre > #partial then
local before = input:sub(1, cursorPos - 1 - #partial)
local after = input:sub(cursorPos)
local newInput = before .. pre .. after
local newCursor = #before + #pre + 1
tabState.last = newInput.."\0"..tostring(newCursor)
tabState.list = candidates
return newInput, newCursor, true
else
showCompletions(candidates)
return input, cursorPos, true
end
end
local function getUserInput()
syscall.devctl(1,"sfgc",3)
syscall.write(1, userhost)
syscall.devctl(1,"sfgc",1)
syscall.write(1, ":")
syscall.devctl(1,"sfgc",10)
syscall.write(1, syscall.getcwd())
syscall.devctl(1,"sfgc",1)
syscall.write(1, "$ ")
local curOffsetStr = syscall.devctl(1, "gpos")
local curOffsetX = tonumber(curOffsetStr:sub(1, curOffsetStr:find(";")-1))
local curOffsetY = tonumber(curOffsetStr:sub(curOffsetStr:find(";")+1))
local input = ""
local blinkState = false
local cursorPos = 1
local history = 0
local dirty = true
local function getGhostSuffix()
if #input == 0 then return "" end
local _, partial, isFirst, partialDir, partialBase = parseForCompletion(input, cursorPos)
if cursorPos ~= #input + 1 then return "" end
local candidates
if isFirst then
if partialDir then
local dir = partialDir:sub(1,1) == "/" and partialDir or (syscall.getcwd().."/"..partialDir)
candidates = listDir(dir, partialBase)
for i, c in ipairs(candidates) do candidates[i] = partialDir..c end
else
candidates = listCommands(partialBase)
end
else
local dir, base
if partialDir then
dir = partialDir:sub(1,1) == "/" and partialDir or (syscall.getcwd().."/"..partialDir)
base = partialBase
else
dir = syscall.getcwd()
base = partialBase
end
candidates = listDir(dir, base)
if partialDir then
for i, c in ipairs(candidates) do candidates[i] = partialDir..c end
end
end
if #candidates == 0 then return "" end
local pre = commonPrefix(candidates)
if #pre > #partial then
return pre:sub(#partial + 1)
end
return ""
end
local function redraw()
syscall.devctl(1,"spos",curOffsetX,curOffsetY)
syscall.write(1, string.sub(input, 1, cursorPos-1))
if blinkState then
syscall.devctl(1,"sfgc",16); syscall.devctl(1,"sbgc",1)
end
if cursorPos > #input then
syscall.write(1, " ")
else
syscall.write(1, string.sub(input, cursorPos, cursorPos))
end
syscall.devctl(1,"sfgc",1); syscall.devctl(1,"sbgc",16)
local after = string.sub(input, cursorPos+1)
syscall.write(1, after)
local ghost = getGhostSuffix()
if #ghost > 0 then
syscall.devctl(1,"sfgc",14)
syscall.write(1, ghost)
syscall.devctl(1,"sfgc",1)
syscall.write(1, " ")
else
syscall.write(1, " ")
end
end
while true do
local key = syscall.read(0)
if key and key ~= "" then
if key=="" then if cursorPos>1 then cursorPos=cursorPos-1;dirty=true end
elseif key=="" then if cursorPos<=#input then cursorPos=cursorPos+1;dirty=true end
elseif key=="" then
if history<#commandHistory then
history=history+1
input=commandHistory[#commandHistory-history+1]
cursorPos=#input+1;dirty=true
end
elseif key=="" then
if history>1 then
history=history-1
input=commandHistory[#commandHistory-history+1]
cursorPos=#input+1;dirty=true
elseif history==1 then
history=0;input="";cursorPos=1;dirty=true
end
elseif key=="" then cursorPos=1;dirty=true
elseif key=="" then cursorPos=#input+1;dirty=true
elseif key=="[3~" then
if cursorPos<=#input then
input=string.sub(input,1,cursorPos-1)..string.sub(input,cursorPos+1)
dirty=true
end
elseif key=="\t" then
local newInput, newCursor, needsRedraw = doTabComplete(input, cursorPos)
if needsRedraw then
input = newInput; cursorPos = newCursor
local posStr = syscall.devctl(1, "gpos")
local sep = posStr:find(";")
local px = tonumber(posStr:sub(1, sep-1))
local py = tonumber(posStr:sub(sep+1))
if px > 1 then
syscall.devctl(1,"spos",1,py)
local tsz = syscall.devctl(1,"size") or "51;19"
local tw = tonumber(tsz:match("^(%d+)")) or 51
syscall.write(1, string.rep(" ", tw))
syscall.devctl(1,"spos",1,py)
end
syscall.devctl(1,"sfgc",3); syscall.write(1, userhost)
syscall.devctl(1,"sfgc",1); syscall.write(1, ":")
syscall.devctl(1,"sfgc",10); syscall.write(1, syscall.getcwd())
syscall.devctl(1,"sfgc",1); syscall.write(1, "$ ")
posStr = syscall.devctl(1, "gpos")
sep = posStr:find(";")
curOffsetX = tonumber(posStr:sub(1, sep-1))
curOffsetY = tonumber(posStr:sub(sep+1))
dirty = true
end
elseif key=="\b" then
if cursorPos>1 then
input=string.sub(input,1,cursorPos-2)..string.sub(input,cursorPos)
cursorPos=cursorPos-1;dirty=true
end
elseif key=="\n" then
syscall.devctl(1,"sfgc",1);syscall.devctl(1,"sbgc",16)
syscall.devctl(1,"spos",curOffsetX,curOffsetY)
syscall.write(1, input.." \n")
return input
elseif #key == 1 and key:byte(1) >= 32 and key:byte(1) < 127 then
input=string.sub(input,1,cursorPos-1)..key..string.sub(input,cursorPos)
cursorPos=cursorPos+1;dirty=true
end
end
local curBlink = ((math.floor(syscall.getUptime()/500)%2)==0)
if curBlink~=blinkState then blinkState=curBlink;dirty=true end
if dirty then redraw();dirty=false end
end
end
local function printError(progName, msg)
syscall.devctl(1,"sfgc",2)
local s = tostring(msg)
local line, rest = s:match("%]:(%d+): (.+)$")
if not line then line, rest = s:match(":(%d+): (.+)$") end
if line then
printInline(progName..": error on line "..line..": "); print(rest)
else
print(progName..": "..s)
end
syscall.devctl(1,"sfgc",1)
end
local function runCommand(command)
do
local func = load("return "..command, "@equation", "t", {})
if func then
local ok, result = pcall(func)
if ok and type(result)=="number" then print(result); return end
end
end
terminate = false
local args = string.split(command, " ")
for i = #args, 1, -1 do
if args[i] == "" then table.remove(args, i) end
end
if #args == 0 then return end
if builtinCmds[args[1]] then
local ok, err = pcall(builtinCmds[args[1]], table.unpack(args, 2))
if not ok then printError(args[1], err) end
return
end
local cmdPath = ""
if string.find(args[1], "/") then
local candidate = args[1]
if candidate:sub(1,1) ~= "/" then candidate = syscall.getcwd().."/"..candidate end
if fs.exists(candidate) then cmdPath = candidate end
else
local paths = string.split(syscall.getEnviron("PATH"), ":")
for _, p in pairs(paths) do
if fs.exists(p..args[1]) then cmdPath = p..args[1]; break end
end
if cmdPath == "" then
local cwd = syscall.getcwd()
local candidate = cwd..(cwd:sub(-1)=="/" and "" or "/")..args[1]
if fs.exists(candidate) then cmdPath = candidate end
end
end
if cmdPath == "" then print(args[1]..": Command not found"); return end
local progName = cmdPath:match("([^/]+)$") or args[1]
local xok, xerr = pcall(syscall.access, cmdPath, "x")
if not xok then
syscall.devctl(1,"sfgc",2); print(progName..": Permission denied"); syscall.devctl(1,"sfgc",1)
return
end
local proc = syscall.spawn(function()
-- Open standard fds so programs that don't do it themselves work correctly.
syscall.open("/dev/tty/1", "r") -- fd 0 stdin
syscall.open("/dev/tty/1", "w") -- fd 1 stdout
syscall.open("/dev/null", "w") -- fd 2 stderr
-- exec replaces this coroutine's code with a fresh isolated environment
-- compiled from disk by the kernel (via loadExecutable -> freshUserEnv),
-- so the child cannot share any upvalue or syscall table state with hysh.
syscall.exec(cmdPath, {table.unpack(args, 2)})
end, progName)
while true do
local exited, code = syscall.collect(proc)
if exited then
if code then print("\nTask exited with code:\n"..tostring(code)) end
return
end
if terminate then
local ok2 = syscall.kill(proc)
if ok2 then
syscall.devctl(1,"sbgc",16); syscall.devctl(1,"sfgc",2)
print("\nProgram Terminated."); syscall.devctl(1,"sfgc",1)
end
terminate = false; break
end
sleep(0.05)
end
end
while true do
local command = getUserInput()
if command ~= "" then
if command ~= commandHistory[#commandHistory] then
table.insert(commandHistory, command)
end
runCommand(command)
end
end
end, debug.traceback)
if not success then
syscall.log("Error running shell: "..errorMsg, "ERROR")
syscall.devctl(1,"sfgc",2)
syscall.devctl(1,"sbgc",16)
print()
print("Error running shell: ")
print(errorMsg)
end