Modular Computers the manual GitHub

A command shell

A prompt that runs commands, the way every operating system’s first shell did. Type a command’s name and its arguments, press Enter, and it runs; then the prompt comes back. Its commands are a tour of the board’s API: the devices on the bus, their methods, calling any of them, waiting for interrupts, files on the disk, and programs. It’s the last page’s OS with a table of commands added.

What you need

The whole thing

Replace /init.lua on your OS disk with this (it’s long to type: download it with wget, or copy it into the disk’s folder; see Booting your own code):

-- /init.lua: a tiny operating system with a command shell. It reads a command, runs it, and
-- reads the next, forever. Its commands talk to the board and its devices: list them, read their
-- methods, call any method, wait for interrupts, read and write files on the boot disk, run a Lua
-- program from it, and turn the computer off. Type help for the list.
local disk = ...  -- the boot disk's address (sata:0, say): what the bootloader passes

local READ_SIZE = 4096 -- bytes a file read asks for at a time
local TICKS_PER_SECOND = 20 -- the board counts time in game ticks

-- ---- the screen and the keyboard ---------------------------------------------------------------

-- Writes text to every screen ("console"). A terminal goes back to the left only on \r.
local function write(text)
  board.invoke("console", "write", (text:gsub("\n", "\r\n")))
end

-- A value as text: tables as {key = value, ...}, so a method's answers can be read.
local function show(value)
  if type(value) == "string" then
    return string.format("%q", value)
  elseif type(value) ~= "table" then
    return tostring(value)
  end
  local parts = {}
  for k, v in pairs(value) do
    parts[#parts + 1] = (type(k) == "string" and k or "[" .. show(k) .. "]") .. " = " .. show(v)
  end
  table.sort(parts)
  return "{" .. table.concat(parts, ", ") .. "}"
end

-- Writes its arguments, separated by spaces, and a new line.
local function print(...)
  local parts = table.pack(...)
  for i = 1, parts.n do
    parts[i] = tostring(parts[i])
  end
  write(table.concat(parts, " ") .. "\n")
end

-- Reads a line: shows what's typed, takes Backspace, ends at Enter.
local function readline(prompt)
  write(prompt)
  local line = ""
  local escape = false -- in the middle of a key's escape sequence (an arrow key sends ESC [ A)
  while true do
    board.waitForInterrupt(nil, "console") -- sleeps until a key is pressed
    local ok, keys = board.invoke("console", "read")
    if ok and keys then
      for i = 1, #keys do
        local key = keys:sub(i, i)
        local byte = key:byte()
        if escape then
          escape = not key:match("[%a~]")
        elseif byte == 27 then
          escape = true
        elseif key == "\r" then
          write("\n")
          return line
        elseif byte == 127 or byte == 8 then
          if #line > 0 then
            line = line:sub(1, -2)
            write("\b \b")
          end
        elseif byte >= 32 then
          line = line .. key
          write(key)
        end
      end
    end
  end
end

-- ---- devices and files -------------------------------------------------------------------------

-- A device's address from what's typed: a type (redstone, the first one on the bus) or an address
-- as it is (sata:0, exp:1, id:<uuid>, console).
local function find(name)
  for _, device in ipairs(board.list()) do
    if device.type == name then
      return device.address
    end
  end
  return name
end

-- The boot disk's file at path, all of it; or nil and why.
local function readfile(path)
  local ok, file = board.invoke(disk, "open", path)
  if not ok then
    return nil, file
  end
  local parts = {}
  while true do
    local read, chunk = board.invoke(file, "read", READ_SIZE)
    if not read or chunk == nil then
      break
    end
    parts[#parts + 1] = chunk
  end
  board.invoke(file, "close")
  return table.concat(parts)
end

-- Writes text to a file on the boot disk (made, or replaced); true, or nil and why.
local function writefile(path, text)
  local ok, file = board.invoke(disk, "open", path, "w")
  if not ok then
    return nil, file
  end
  board.invoke(file, "write", text)
  local closed, why = board.invoke(file, "close") -- (a disk keeps what's written at close)
  if not closed then
    return nil, why
  end
  return true
end

-- ---- the commands ------------------------------------------------------------------------------

-- Each command: a function given the words after its name, and the whole line as typed.
local commands = {}

commands.help = function()
  print("devices                    the devices on the bus")
  print("methods <device>           a device's methods, and what each does")
  print("call <device> <method> ... calls a method; its arguments are Lua values: 1 \"text\" {a = 1}")
  print("wait [seconds]             waits for an interrupt from any device but the keyboard")
  print("ls [dir]                   the files in a directory on the boot disk")
  print("cat <file>                 shows a file")
  print("save <file> <text>         writes the text to a file")
  print("run <file> [words]         runs a Lua program from the boot disk")
  print("uptime                     seconds since the computer started")
  print("reboot, off                restarts, or turns off, the computer")
end

commands.devices = function()
  for _, device in ipairs(board.list()) do
    print(device.address, device.type, device.id)
  end
end

commands.methods = function(words)
  local address = find(words[1] or "")
  local names = board.methods(address)
  if not names then
    print("no device " .. (words[1] or ""))
    return
  end
  for _, method in ipairs(names) do
    print(board.doc(address, method))
  end
end

commands.call = function(words, rest)
  local address, method = find(words[1] or ""), words[2]
  if not method then
    print("usage: call <device> <method> [arguments]")
    return
  end
  -- The arguments: whatever follows the method's name on the line (after "call", the device and
  -- the method), read as Lua values (with nothing else visible to them: no globals).
  local argtext = rest:match("^%s*%S+%s+%S+%s+%S+%s*(.*)$") or ""
  local chunk, why = load("return " .. argtext, "=arguments", "t", {})
  if not chunk then
    print("bad arguments: " .. why)
    return
  end
  local results = table.pack(board.invoke(address, method, chunk()))
  if not results[1] then
    print("failed: " .. tostring(results[2]))
    return
  end
  for i = 2, results.n do
    print(show(results[i]))
  end
end

commands.wait = function(words)
  -- Every device but the terminals (their keys are the shell's), and "bus": the bus's own
  -- interrupt, raised when a device joins or leaves.
  local sources = {"bus"}
  for _, device in ipairs(board.list()) do
    if device.type ~= "terminal" then
      sources[#sources + 1] = device.address
    end
  end
  local raised = board.waitForInterrupt(tonumber(words[1]), table.unpack(sources))
  if #raised == 0 then
    print("nothing")
  end
  for _, source in ipairs(raised) do
    print(source) -- still raised until its data is taken: call it (pull, read, changes...)
  end
end

commands.ls = function(words)
  local ok, names = board.invoke(disk, "list", words[1] or "/")
  if not ok then
    print(names)
    return
  end
  for _, name in ipairs(names or {}) do
    print(name)
  end
end

commands.cat = function(words)
  local text, why = readfile(words[1] or "")
  write(text or (tostring(why) .. "\n"))
end

commands.save = function(words, rest)
  local text = rest:match("^%s*%S+%s+%S+%s(.*)$") or "" -- (the line after "save" and the file, as typed)
  local ok, why = writefile(words[1] or "", text .. "\n")
  if not ok then
    print(why)
  end
end

commands.run = function(words)
  local path = table.remove(words, 1) or ""
  local text, why = readfile(path)
  if not text then
    print(why)
    return
  end
  -- The program sees the OS's print, write and readline, its words as arg, and every global.
  local env = setmetatable({print = print, write = write, readline = readline, disk = disk, arg = words}, {__index = _G})
  local program, err = load(text, "=" .. path, "t", env)
  if not program then
    print(err)
    return
  end
  program(table.unpack(words))
end

commands.uptime = function()
  print(board.uptime() / TICKS_PER_SECOND) -- (the board's uptime is in ticks)
end

commands.reboot = function()
  board.reboot()
end

commands.off = function()
  board.poweroff()
end

-- ---- the shell ---------------------------------------------------------------------------------

print("tiny OS, booted from " .. disk .. ". Type help.")
while true do
  local line = readline("# ")
  local words = {}
  for word in line:gmatch("%S+") do
    words[#words + 1] = word
  end
  local name = table.remove(words, 1)
  if name then
    local command = commands[name]
    if command then
      -- A command that fails says why; the shell goes on (an error here would stop the CPU).
      local ok, err = pcall(command, words, line)
      if not ok then
        print("error: " .. tostring(err))
      end
    else
      print(name .. ": no such command")
    end
  end
end

Boot it and try each command:

tiny OS, booted from sata:0. Type help.
# help
devices                    the devices on the bus
...
# devices
eeprom eeprom id:6f1c...
sata:0 storage id:9a02...
id:41be... terminal id:41be...
# methods storage
changes(): the oldest change on it not yet taken, ...
getCapacity(): how many bytes it holds
...
# call sata:0 getCapacity
1048576
# save /hello.txt Hello, disk!
# cat /hello.txt
Hello, disk!
# ls
hello.txt
init.lua
# uptime
41.5
# off

The commands

Command What it does The board call
help lists the commands  
devices each device on the bus: its address, type and id board.list()
methods <device> every method of a device, with what it does board.methods, board.doc
call <device> <method> [args] calls a method, and shows what it answered board.invoke
wait [seconds] waits for any device’s interrupt, and shows which board.waitForInterrupt
ls [dir] the files in a folder of the boot disk the disk’s list
cat <file> shows a file the disk’s open, then the file’s read and close
save <file> <text> writes the text (the rest of the line) to a file the disk’s open, the file’s write, close
run <file> [words] runs a Lua program from the boot disk load
uptime seconds since the computer started board.uptime(), in ticks
reboot, off restarts the computer, or turns it off board.reboot(), board.poweroff()

A <device> is an address (sata:0, exp:1, id:..., console) or a type (redstone, meaning the first redstone card on the bus).

How it works

Reading a command

The loop at the bottom reads a line with readline (the last page’s), splits it into words at spaces, and looks the first word up in the commands table. Each command is a function given the other words, and the whole line for a command that wants it as typed (save keeps the spaces in your text; call reads its arguments as Lua).

The command runs inside pcall, so a mistake (a bad argument, a bug in a command) is printed as error: ... and the shell goes on. Without it, any error would end your OS, and the CPU would abort.

Calling any method

call is the most powerful command: anything a device can do, you can do from the prompt. Everything after the method’s name is read as Lua values, so you write arguments as you would in a program: numbers, "strings" in quotes, true, and tables like {north = {powered = 15}}. Its answers are shown the same way, tables as {key = value}.

# call sata:0 stat "/init.lua"
{directory = false, modified = 1200850, size = 8431}
# call sata:0 nope
failed: no such method: nope

A call never stops the computer. When it fails, board.invoke answers false and why, and call shows failed: and the message.

Files on the disk

A disk on its own knows nothing about paths beyond its own files: open gives a handle, an address of its own, and you call read, write and close on the handle as on a device. A file’s new contents are kept when it’s closed, all at once. readfile and writefile wrap those calls. See Hard disk for everything a disk does.

Running programs

run reads a Lua file from the disk, loads it, and calls it with its words. The program sees the OS’s print, write and readline, its words as arg, and every global (board among them). The shell waits while it runs, and its prompt comes back when the program returns. There’s an example on the next page.

Adding a command

A command is one function in the table. To add echo, which writes its words back:

commands.echo = function(words)
  print(table.concat(words, " "))
end

Put it with the other commands (before the shell’s loop at the bottom), and add a line to help.

When it goes wrong

Next: Talking to devices.