Минималистичная версия. 1 юзер, базовая fs, man по командам хардкодом

This commit is contained in:
2026-06-14 02:48:14 +03:00
parent 14f47767b3
commit 171c45df8b
2 changed files with 264 additions and 61 deletions

View File

@@ -8,7 +8,8 @@ local ST_IDLE = 0
local ST_WAIT_FS = 1 local ST_WAIT_FS = 1
local state = ST_IDLE local state = ST_IDLE
local uid, gid, cwd, curUser = 0, 0, "/", "root" local uid, gid, curUser = 0, 0, "root"
local cwd = curUser == "root" and "/" or "/home/" .. curUser
local lastRespSeq = 0 local lastRespSeq = 0
local outQueue = {} local outQueue = {}
@@ -19,11 +20,34 @@ local function parseMsg(msg)
return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil
end end
local function normalizePath(path)
local parts = {}
for seg in path:gmatch("[^/]+") do
if seg == ".." then
if #parts > 0 then table.remove(parts) end
elseif seg ~= "." then
parts[#parts + 1] = seg
end
end
if #parts == 0 then return "/" end
return "/" .. table.concat(parts, "/")
end
local function expandHome(path)
local home = curUser == "root" and "/" or "/home/" .. curUser
local rest = path:sub(2)
if rest == "" or rest:sub(1, 1) == "/" then
return normalizePath(home .. rest)
end
return normalizePath(home .. "/" .. rest)
end
local function resolvePath(path) local function resolvePath(path)
if not path or path == "" then return cwd end if not path or path == "" then return cwd end
if path:sub(1, 1) == "/" then return path end if path:sub(1, 1) == "~" then return expandHome(path) end
if cwd:sub(-1) == "/" then return cwd .. path end if path:sub(1, 1) == "/" then return normalizePath(path) end
return cwd .. "/" .. path local full = cwd .. (cwd:sub(-1) == "/" and "" or "/") .. path
return normalizePath(full)
end end
local function fsReq(op, arg) local function fsReq(op, arg)
@@ -55,46 +79,78 @@ local function queueLines(lines)
end end
end end
-- centralized argument validation
local valid = {}
function valid.minArgs(args, n, msg)
if #args < n then error("usage: " .. msg) end
end
function valid.maxArgs(args, n, msg)
if #args > n then error("usage: " .. msg) end
end
function valid.exactArgs(args, n, msg)
if #args ~= n then error("usage: " .. msg) end
end
function valid.num(v, name)
local n = tonumber(v)
if not n then error(name .. ": expected number, got '" .. tostring(v) .. "'") end
return math.floor(math.max(0, math.min(255, n)))
end
function valid.flags(args, allowed)
local opts, positional = {}, nil
for _, arg in ipairs(args) do
if arg:sub(1, 1) == "-" then
for j = 2, #arg do
local c = arg:sub(j, j)
if not allowed[c] then error("unknown option: -" .. c) end
opts[c] = true
end
else
positional = arg
end
end
return opts, positional
end
local cmds = {} local cmds = {}
local function reg(name, fn, desc) local function reg(name, fn, desc, usage)
cmds[name] = { fn = fn, desc = desc } cmds[name] = { fn = fn, desc = desc, usage = usage }
end end
reg("help", function(a) reg("help", function(a)
local lines = { curUser .. " " .. cwd .. "> help" } local lines = { curUser .. " " .. cwd .. "> help" }
lines[#lines + 1] = "AtlasOS v2.0:" lines[#lines + 1] = "AtlasOS v2.0:"
for n, e in pairs(cmds) do for n, e in pairs(cmds) do
lines[#lines + 1] = " " .. n .. " " .. e.desc local u = e.usage and " " .. e.usage or ""
lines[#lines + 1] = " " .. n .. u .. " " .. e.desc
end end
queueLines(lines) queueLines(lines)
end, "This help") end, "This help")
reg("echo", function(a) reg("echo", function(a)
if #a == 0 then error("echo: missing text") end valid.minArgs(a, 1, "echo <text>")
local out = "" local out = table.concat(a, " ")
for i = 1, #a do
if #out > 0 then out = out .. " " end
out = out .. tostring(a[i])
end
queueLines({ curUser .. " " .. cwd .. "> echo " .. out, out }) queueLines({ curUser .. " " .. cwd .. "> echo " .. out, out })
end, "Echo text") end, "Echo text", "<text>")
reg("clear", function(a) reg("clear", function(a)
qClear() qClear()
end, "Clear screen") end, "Clear screen")
reg("color", function(a) reg("color", function(a)
if #a < 3 then error("color: need R G B") end valid.exactArgs(a, 3, "color <R> <G> <B>")
local r, g, b = tonumber(a[1]), tonumber(a[2]), tonumber(a[3]) local r = valid.num(a[1], "color")
if not (r and g and b) then error("color: invalid values") end local g = valid.num(a[2], "color")
r = math.floor(math.max(0, math.min(255, r))) local b = valid.num(a[3], "color")
g = math.floor(math.max(0, math.min(255, g)))
b = math.floor(math.max(0, math.min(255, b)))
qColor(r, g, b) qColor(r, g, b)
queueLines({ curUser .. " " .. cwd .. "> color", queueLines({ curUser .. " " .. cwd .. "> color",
"Color set to " .. r .. "," .. g .. "," .. b }) "Color set to " .. r .. "," .. g .. "," .. b })
end, "Set color R G B") end, "Set color", "<R> <G> <B>")
reg("status", function(a) reg("status", function(a)
pendingCmd = { name = "status", pendingCmd = { name = "status",
@@ -104,57 +160,51 @@ reg("status", function(a)
end, "System status") end, "System status")
reg("ls", function(a) reg("ls", function(a)
local path = cwd local opts, path = valid.flags(a, { a = true, l = true })
local opts = {} path = resolvePath(path or cwd)
for i = 1, #a do
if a[i]:sub(1, 1) == "-" then
local opt = a[i]
for j = 2, #opt do
local c = opt:sub(j, j)
if c ~= "a" and c ~= "l" then
error("ls: unknown option '" .. opt .. "'")
end
end
opts[opt] = true
else
path = resolvePath(a[i])
end
end
local cmdStr = "ls" local cmdStr = "ls"
for i = 1, #a do for i = 1, #a do cmdStr = cmdStr .. " " .. tostring(a[i]) end
cmdStr = cmdStr .. " " .. tostring(a[i])
end
pendingCmd = { name = "ls", opts = opts, path = path, pendingCmd = { name = "ls", opts = opts, path = path,
prompt = curUser .. " " .. cwd .. "> " .. cmdStr } prompt = curUser .. " " .. cwd .. "> " .. cmdStr }
state = ST_WAIT_FS state = ST_WAIT_FS
fsReq("LS", path) fsReq("LS", path)
end, "List directory [path]") end, "List directory", "[-la] [path]")
reg("cat", function(a) reg("cat", function(a)
if #a == 0 then error("cat: need path") end valid.minArgs(a, 1, "cat <path>")
local path = resolvePath(a[1]) local path = resolvePath(a[1])
pendingCmd = { name = "cat", path = path, pendingCmd = { name = "cat", path = path,
prompt = curUser .. " " .. cwd .. "> cat " .. a[1] } prompt = curUser .. " " .. cwd .. "> cat " .. a[1] }
state = ST_WAIT_FS state = ST_WAIT_FS
fsReq("READ", path .. "|0|8192") fsReq("READ", path .. "|0|8192")
end, "Show file content") end, "Show file content", "<path>")
reg("cd", function(a) reg("cd", function(a)
valid.maxArgs(a, 1, "cd [path]")
local path = a[1] and resolvePath(a[1]) or "/" local path = a[1] and resolvePath(a[1]) or "/"
pendingCmd = { name = "cd", path = path, pendingCmd = { name = "cd", path = path,
prompt = curUser .. " " .. cwd .. "> cd " .. (a[1] or "") } prompt = curUser .. " " .. cwd .. "> cd " .. (a[1] or "") }
state = ST_WAIT_FS state = ST_WAIT_FS
fsReq("CHDIR", path) fsReq("CHDIR", path)
end, "Change directory [path]") end, "Change directory", "[path]")
reg("stat", function(a) reg("stat", function(a)
if #a == 0 then error("stat: need path") end valid.minArgs(a, 1, "stat <path>")
local path = resolvePath(a[1]) local path = resolvePath(a[1])
pendingCmd = { name = "stat", path = path, pendingCmd = { name = "stat", path = path,
prompt = curUser .. " " .. cwd .. "> stat " .. a[1] } prompt = curUser .. " " .. cwd .. "> stat " .. a[1] }
state = ST_WAIT_FS state = ST_WAIT_FS
fsReq("STAT", path) fsReq("STAT", path)
end, "Show file metadata") end, "Show file metadata", "<path>")
reg("man", function(a)
valid.exactArgs(a, 1, "man <command>")
local cmd = a[1]
pendingCmd = { name = "man", cmd = cmd,
prompt = curUser .. " " .. cwd .. "> man " .. cmd }
state = ST_WAIT_FS
fsReq("READ", "/etc/man/" .. cmd .. "|0|8192")
end, "Display manual page", "<command>")
local function execCmd(cmdStr) local function execCmd(cmdStr)
local parts = {} local parts = {}
@@ -189,20 +239,24 @@ local function handleFSResponse(op, data)
if op == "ERR" then if op == "ERR" then
local code = data or "EIO" local code = data or "EIO"
local m = cmd.path
if code == "ENOENT" then
m = m .. ": No such file or directory"
elseif code == "EACCES" then
m = m .. ": Permission denied"
elseif code == "ENOTDIR" then
m = m .. ": Not a directory"
elseif code == "EISDIR" then
m = m .. ": Is a directory"
else
m = m .. ": " .. code
end
qText(cmd.prompt) qText(cmd.prompt)
qErr(m) if cmd.name == "man" then
qErr("No manual entry for " .. cmd.cmd)
else
local m = cmd.path
if code == "ENOENT" then
m = m .. ": No such file or directory"
elseif code == "EACCES" then
m = m .. ": Permission denied"
elseif code == "ENOTDIR" then
m = m .. ": Not a directory"
elseif code == "EISDIR" then
m = m .. ": Is a directory"
else
m = m .. ": " .. code
end
qErr(m)
end
pendingCmd = nil pendingCmd = nil
state = ST_IDLE state = ST_IDLE
return return
@@ -211,7 +265,7 @@ local function handleFSResponse(op, data)
local lines = { cmd.prompt } local lines = { cmd.prompt }
if cmd.name == "ls" then if cmd.name == "ls" then
local showAll = (cmd.opts["-la"] or cmd.opts["-a"]) local showAll = cmd.opts["a"] or false
local names = {} local names = {}
for n in data:gmatch("([^|]+)") do for n in data:gmatch("([^|]+)") do
names[#names + 1] = n names[#names + 1] = n
@@ -260,6 +314,15 @@ local function handleFSResponse(op, data)
else else
lines[#lines + 1] = data lines[#lines + 1] = data
end end
elseif cmd.name == "man" then
if data and #data > 0 then
for line in data:gmatch("[^\n]+") do
lines[#lines + 1] = line
end
else
lines[#lines + 1] = "(no content)"
end
end end
queueLines(lines) queueLines(lines)

View File

@@ -233,7 +233,147 @@ local function defaultFS()
mkFile(3, "Welcome to AtlasOS v2.0!\n") mkFile(3, "Welcome to AtlasOS v2.0!\n")
mkIno(4, "d", "755", 0, 0, 0) mkIno(4, "d", "755", 0, 0, 0)
mkDir(4, { { name = ".", ino = 4 }, { name = "..", ino = 1 } }) mkDir(4, { { name = ".", ino = 4 }, { name = "..", ino = 1 }, { name = "man", ino = 6 } })
mkIno(6, "d", "755", 0, 0, 0)
mkDir(6, {
{ name = ".", ino = 6 }, { name = "..", ino = 4 },
{ name = "help", ino = 7 }, { name = "echo", ino = 8 },
{ name = "clear", ino = 9 }, { name = "color", ino = 10 },
{ name = "status", ino = 11 }, { name = "ls", ino = 12 },
{ name = "cat", ino = 13 }, { name = "cd", ino = 14 },
{ name = "stat", ino = 15 }, { name = "man", ino = 16 },
})
mkIno(7, "f", "644", 0, 0, 0)
mkFile(7, [[NAME
help - display help information
SYNOPSIS
help
DESCRIPTION
Show a list of all available commands with their usage
and description.
]])
mkIno(8, "f", "644", 0, 0, 0)
mkFile(8, [[NAME
echo - display a line of text
SYNOPSIS
echo <text>
DESCRIPTION
Write the given text to the terminal output.
]])
mkIno(9, "f", "644", 0, 0, 0)
mkFile(9, [[NAME
clear - clear the terminal screen
SYNOPSIS
clear
DESCRIPTION
Clear all text from the terminal display.
]])
mkIno(10, "f", "644", 0, 0, 0)
mkFile(10, [[NAME
color - set terminal text color
SYNOPSIS
color <R> <G> <B>
DESCRIPTION
Change the text color for subsequent output.
Each value must be 0-255 (red, green, blue).
]])
mkIno(11, "f", "644", 0, 0, 0)
mkFile(11, [[NAME
status - show system status
SYNOPSIS
status
DESCRIPTION
Display system information including current user,
working directory, and memory usage.
]])
mkIno(12, "f", "644", 0, 0, 0)
mkFile(12, [[NAME
ls - list directory contents
SYNOPSIS
ls [-la] [path]
DESCRIPTION
List contents of a directory. If no path is given,
list the current directory.
OPTIONS
-a Include hidden entries (. and ..)
-l Long format (detailed listing)
]])
mkIno(13, "f", "644", 0, 0, 0)
mkFile(13, [[NAME
cat - concatenate and display files
SYNOPSIS
cat <path>
DESCRIPTION
Read a file and display its contents on the terminal.
]])
mkIno(14, "f", "644", 0, 0, 0)
mkFile(14, [[NAME
cd - change the working directory
SYNOPSIS
cd [path]
DESCRIPTION
Change the current working directory. If no path
is given, go to root (/). Supports both absolute
and relative paths.
]])
mkIno(15, "f", "644", 0, 0, 0)
mkFile(15, [[NAME
stat - show file metadata
SYNOPSIS
stat <path>
DESCRIPTION
Display metadata for a file or directory, including
inode number, type, permissions, owner, group, size,
and modification time.
]])
mkIno(16, "f", "644", 0, 0, 0)
mkFile(16, [[NAME
man - display manual page
SYNOPSIS
man <command>
DESCRIPTION
Display the manual page for a command. Manual pages
are stored in /etc/man/.
EXAMPLE
man ls
man cat
SEE ALSO
help
]])
local str = fsSerialize() local str = fsSerialize()
out[5] = str out[5] = str