From 171c45df8bb96bd01ac74e00f53b2940dc6f40c7 Mon Sep 17 00:00:00 2001 From: SlavaVlad Date: Sun, 14 Jun 2026 02:48:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B8=D1=87=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8F.=201=20=D1=8E=D0=B7=D0=B5=D1=80,=20?= =?UTF-8?q?=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0=D1=8F=20fs,=20man=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0=D0=BC=20?= =?UTF-8?q?=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atlas_os/CMC.lua | 183 +++++++++++++++++++++++++++++++---------------- atlas_os/MMC.lua | 142 +++++++++++++++++++++++++++++++++++- 2 files changed, 264 insertions(+), 61 deletions(-) diff --git a/atlas_os/CMC.lua b/atlas_os/CMC.lua index b5c9f0e..820a371 100644 --- a/atlas_os/CMC.lua +++ b/atlas_os/CMC.lua @@ -8,7 +8,8 @@ local ST_IDLE = 0 local ST_WAIT_FS = 1 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 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 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) if not path or path == "" then return cwd end - if path:sub(1, 1) == "/" then return path end - if cwd:sub(-1) == "/" then return cwd .. path end - return cwd .. "/" .. path + if path:sub(1, 1) == "~" then return expandHome(path) end + if path:sub(1, 1) == "/" then return normalizePath(path) end + local full = cwd .. (cwd:sub(-1) == "/" and "" or "/") .. path + return normalizePath(full) end local function fsReq(op, arg) @@ -55,46 +79,78 @@ local function queueLines(lines) 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 function reg(name, fn, desc) - cmds[name] = { fn = fn, desc = desc } +local function reg(name, fn, desc, usage) + cmds[name] = { fn = fn, desc = desc, usage = usage } end reg("help", function(a) local lines = { curUser .. " " .. cwd .. "> help" } lines[#lines + 1] = "AtlasOS v2.0:" 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 queueLines(lines) end, "This help") reg("echo", function(a) - if #a == 0 then error("echo: missing text") end - local out = "" - for i = 1, #a do - if #out > 0 then out = out .. " " end - out = out .. tostring(a[i]) - end + valid.minArgs(a, 1, "echo ") + local out = table.concat(a, " ") queueLines({ curUser .. " " .. cwd .. "> echo " .. out, out }) -end, "Echo text") +end, "Echo text", "") reg("clear", function(a) qClear() end, "Clear screen") reg("color", function(a) - if #a < 3 then error("color: need R G B") end - local r, g, b = tonumber(a[1]), tonumber(a[2]), tonumber(a[3]) - if not (r and g and b) then error("color: invalid values") end - r = math.floor(math.max(0, math.min(255, r))) - g = math.floor(math.max(0, math.min(255, g))) - b = math.floor(math.max(0, math.min(255, b))) + valid.exactArgs(a, 3, "color ") + local r = valid.num(a[1], "color") + local g = valid.num(a[2], "color") + local b = valid.num(a[3], "color") qColor(r, g, b) queueLines({ curUser .. " " .. cwd .. "> color", "Color set to " .. r .. "," .. g .. "," .. b }) -end, "Set color R G B") +end, "Set color", " ") reg("status", function(a) pendingCmd = { name = "status", @@ -104,57 +160,51 @@ reg("status", function(a) end, "System status") reg("ls", function(a) - local path = cwd - local opts = {} - 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 opts, path = valid.flags(a, { a = true, l = true }) + path = resolvePath(path or cwd) local cmdStr = "ls" - for i = 1, #a do - cmdStr = cmdStr .. " " .. tostring(a[i]) - end + for i = 1, #a do cmdStr = cmdStr .. " " .. tostring(a[i]) end pendingCmd = { name = "ls", opts = opts, path = path, prompt = curUser .. " " .. cwd .. "> " .. cmdStr } state = ST_WAIT_FS fsReq("LS", path) -end, "List directory [path]") +end, "List directory", "[-la] [path]") reg("cat", function(a) - if #a == 0 then error("cat: need path") end + valid.minArgs(a, 1, "cat ") local path = resolvePath(a[1]) pendingCmd = { name = "cat", path = path, prompt = curUser .. " " .. cwd .. "> cat " .. a[1] } state = ST_WAIT_FS fsReq("READ", path .. "|0|8192") -end, "Show file content") +end, "Show file content", "") reg("cd", function(a) + valid.maxArgs(a, 1, "cd [path]") local path = a[1] and resolvePath(a[1]) or "/" pendingCmd = { name = "cd", path = path, prompt = curUser .. " " .. cwd .. "> cd " .. (a[1] or "") } state = ST_WAIT_FS fsReq("CHDIR", path) -end, "Change directory [path]") +end, "Change directory", "[path]") reg("stat", function(a) - if #a == 0 then error("stat: need path") end + valid.minArgs(a, 1, "stat ") local path = resolvePath(a[1]) pendingCmd = { name = "stat", path = path, prompt = curUser .. " " .. cwd .. "> stat " .. a[1] } state = ST_WAIT_FS fsReq("STAT", path) -end, "Show file metadata") +end, "Show file metadata", "") + +reg("man", function(a) + valid.exactArgs(a, 1, "man ") + 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", "") local function execCmd(cmdStr) local parts = {} @@ -189,20 +239,24 @@ local function handleFSResponse(op, data) if op == "ERR" then 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) - 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 state = ST_IDLE return @@ -211,7 +265,7 @@ local function handleFSResponse(op, data) local lines = { cmd.prompt } if cmd.name == "ls" then - local showAll = (cmd.opts["-la"] or cmd.opts["-a"]) + local showAll = cmd.opts["a"] or false local names = {} for n in data:gmatch("([^|]+)") do names[#names + 1] = n @@ -260,6 +314,15 @@ local function handleFSResponse(op, data) else lines[#lines + 1] = data 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 queueLines(lines) diff --git a/atlas_os/MMC.lua b/atlas_os/MMC.lua index b88d018..6c32c05 100644 --- a/atlas_os/MMC.lua +++ b/atlas_os/MMC.lua @@ -233,7 +233,147 @@ local function defaultFS() mkFile(3, "Welcome to AtlasOS v2.0!\n") 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 + +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 + +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 + +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 + +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 + +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() out[5] = str