Programs in several files
Split a program into modules with import, organize a project in folders, start it from startup, and share code between computers.
A program that drives a whole factory soon grows to hundreds of lines: reading the vaults, drawing the screen, talking to other computers. Past a certain size, one file is hard to read and to change. Brass lets you split it: each file does one job, and import brings the pieces together. The same pieces can then be reused by your other programs: write a progress bar once, use it everywhere.
A first module
A module is an ordinary file of the medium that ends with return and a value, nearly always a table of functions. Here is a small library of text helpers, saved as lib/text (edit lib/text creates the folder and the file):
local M = {}
-- "Iron" -> "Iron " (cut or filled with spaces to the width)
function M.pad(text, width)
text = tostring(text)
if #text >= width then
return text:sub(1, width)
end
return text .. string.rep(" ", width - #text)
end
-- 1234567 -> "1,234,567"
function M.thousands(n)
if n < 0 then
return "-" .. M.thousands(-n)
end
local digits = tostring(math.floor(n))
local out = ""
while #digits > 3 do
out = "," .. digits:sub(-3) .. out
digits = digits:sub(1, -4)
end
return digits .. out
end
return MA program uses it with import, which runs the file and gives back the table it returns:
local text = import "lib/text"
print(text.pad("Iron Ingot", 12) .. text.thousands(1234567))
print(text.pad("Gold", 12) .. text.thousands(87))The names are yours: text here could be t or txt. By habit, a module builds a table called M (for module) and returns it at the end.
import and require
import(path) runs the file at path and returns what that file returns. require is the same function under the name ComputerCraft players know. Since a call with a single string needs no parentheses, the usual form is:
local text = import "lib/text"On its own line, import "lib/setup" runs a file and ignores what it returns: handy for a file that only defines global functions or prepares the screen.
A file without a return gives nil. A module can return anything (a number, a string, a function), but a table is what you want in nearly every case.
Where the file is looked for
The path is **relative to the folder of the file that calls import**, not to the folder you typed the command in. A file in lib that imports "text" gets lib/text.
| path | from apps/stock/main, means |
|---|---|
"screen" | apps/stock/screen (same folder) |
"parts/bars" | apps/stock/parts/bars |
"../door/main" | apps/door/main (.. goes up one folder) |
"/lib/text" | lib/text (a leading / starts at the root of the medium) |
Lines typed at the brass prompt import relative to the current folder (the one cd chose). File names have no extension: import "lib/text", not "lib/text.lua".
When the file cannot be loaded, the program stops with an error that names the path as you wrote it:
| message | meaning |
|---|---|
cannot import 'lib/txt': no such file | no file at that path (check the spelling, and the folder of the importing file) |
cannot import 'lib': it is a folder | the path names a folder |
cannot import '../../x': no folder above the root | too many .. |
cannot import 'my lib': invalid file name 'my lib' | names use letters, digits, _, . and - only |
no storage medium | the computer has no disk |
A module runs once per program
The first import of a file runs it. Every later import of the same file, from any file of the program, returns the same value without running the file again. To see it in one go, this test writes a tiny module with fs.write, then imports it twice:
fs.write("lib/counter", [[
print("lib/counter runs")
local M = {count = 0}
function M.add()
M.count = M.count + 1
end
return M
]])
local a = import "lib/counter"
local b = import "lib/counter"
a.add()
b.add()
print(a == b, a.count)lib/counter runs true 2
The file ran once, and a and b are the same table: a module is a good place for state shared by all the files of a program (a cache of readings, the current settings).
The memory of what was imported lasts until the program ends. The next run imports everything again, so after editing a module, just run the program again: no reboot needed. At the brass prompt, each line is its own small program, so an import there runs the file every time.
What a module shares, and what it keeps
- The **
localvariables and functions** at the top level of a module are its own. No other file can see them, even with the same name: two modules can both have alocal function draw()without trouble. - Global variables and functions (declared without
local) are shared by every file of the program.
fs.write("lib/shared", [[
local secret = "only inside lib/shared"
farm_name = "Iron Farm"
function shout(message)
print(message:upper())
end
]])
import "lib/shared"
print(secret)
print(farm_name)
shout("hello")nil Iron Farm HELLO
Keep everything local and return what other files need in the module's table. Globals look convenient, but two modules that pick the same global name overwrite each other's value, and nothing warns you.
Inside a module, functions may use the module's top-level locals (like M above): the rule that a function cannot use the locals of an enclosing function does not apply to the top level of a file (see Functions).
Circular imports
If lib/a imports lib/b while lib/b imports lib/a, neither can finish first. Brass stops the program instead of looping forever:
fs.write("lib/a", 'import "b"\nreturn {}')
fs.write("lib/b", 'import "a"\nreturn {}')
import "lib/a"lib/b:1: circular import: 'lib/a' is still being im ported
The cure is to cut the circle: move what both files need into a third module that imports neither of them, or pass the value as a parameter to a function instead of importing it.
Errors in a module
An error inside a module carries the module's own file name and line, so you know which file to open:
fs.write("lib/broken", "local x = \nreturn x")
import "lib/broken"lib/broken:2: unexpected symbol near 'return'
A compile error in a module appears when it is imported, not when the main program starts: everything before the import line has already run. A module whose run failed is run again the next time it is imported.
A module with settings
A settings file written in Brass
A file that just returns a table is a clear and safe way to keep the settings of a program apart from its code. A player can change the settings without reading the program, and comments explain each value:
return {
title = "IRON FARM STOCK",
refresh = 5, -- seconds between two readings
alarm_side = "top", -- lamp lit when a stock is low
items = {
{name = "Iron Ingot", id = "minecraft:iron_ingot", max = 2000},
{name = "Iron Nugget", id = "minecraft:iron_nugget", max = 1000},
{name = "Crushed Iron", id = "create:crushed_raw_iron", max = 500},
},
}local config = import "/config"
print(config.title)Unlike a key = value text file (see Working with text), this one can hold lists and tables inside tables, and needs no reader. The price: a typo in it is a compile error, reported as config:5: ....
Defaults that a program can change
Since every file gets the same table, a module can offer default values that the main program changes once at the start:
local M = {side = "top", level = 15}
function M.ring()
rs.set(M.side, M.level)
end
function M.stop()
rs.set(M.side, 0)
end
return Mlocal alarm = import "/lib/alarm"
alarm.side = "back" -- this computer has its lamp at the back
alarm.ring()Several objects from one module
When you need several of the same thing (three lamps, two presses), the module gives a function that builds a new table for each one. In Lua you would keep each object's data in a closure; Brass functions cannot capture the locals of another function, so the data goes in the table, and the functions receive the table as self:
local Lamp = {}
local function on(self)
rs.set(self.side, self.level)
self.lit = true
end
local function off(self)
rs.set(self.side, 0)
self.lit = false
end
local function toggle(self)
if self.lit then
off(self)
else
on(self)
end
end
function Lamp.new(side, level)
return {side = side, level = level or 15, lit = false, on = on, off = off, toggle = toggle}
end
return Lamplocal Lamp = import "/lib/lamp"
local alarm = Lamp.new("top")
local beacon = Lamp.new("back", 7)
alarm:on()
beacon:toggle()alarm:on() is short for alarm.on(alarm): the colon passes the table as the first argument, self. This is for your own objects. The devices returned by peripheral.wrap are different: their functions are called with a dot, lamp.set(15) (see Peripherals).
Organizing a project in folders
A layout that scales well: shared libraries in lib, one folder per program in apps, the settings at the root, and a tiny startup.
/startup starts the program of this computer
/config the settings of this computer
/lib/text text helpers (pad, thousands)
/lib/ui drawing helpers (bars, rows)
/apps/stock/main the stock screen
/apps/door/main the door controllerThe shell commands work with paths and folders:
> mkdir lib > edit lib/text > edit apps/stock > cd apps/stock /apps/stock> main
edit apps/stock opens the editor with the tree of the folder on the left: click a file to open it, several stay open side by side. The shell and the editor has the details.
A few habits help:
- Import shared libraries with a path from the root,
import "/lib/text": the line is the same in every file, wherever it is. - Import the files of the same program with a relative path,
import "screen": the folder can be renamed or copied without changing the code. - Names of files and folders: letters, digits,
_,.and-, 32 characters at most; a path holds 128 characters; a medium holds 1024 files and folders.
Starting from startup
At boot, a computer runs the file called startup at the root of its medium. Keep it to one line that imports the main program:
import "/apps/stock/main"import is the way for one program to start another: the main program runs inside it, and since it usually loops forever, startup never ends. If it does end, the prompt comes back.
To choose the program without editing startup, read its name from a file:
local name = fs.read("/autorun") -- for example "stock", written with: edit autorun
if name ~= nil then
import("/apps/" .. name:trim() .. "/main")
endA complete example
The stock screen of the layout above, in four files: two libraries, the settings (the config file shown earlier) and the program. It finds a vault or a chest next to the computer (or on its Data Cable), and shows a bar per item, red when the stock is low.
local M = {}
function M.pad(text, width)
text = tostring(text)
if #text >= width then
return text:sub(1, width)
end
return text .. string.rep(" ", width - #text)
end
function M.thousands(n)
if n < 0 then
return "-" .. M.thousands(-n)
end
local digits = tostring(math.floor(n))
local out = ""
while #digits > 3 do
out = "," .. digits:sub(-3) .. out
digits = digits:sub(1, -4)
end
return digits .. out
end
return Mlocal text = import "text" -- the same folder: lib/text
local M = {}
-- a horizontal bar: the filled part in color, the rest in gray
function M.bar(x, y, width, fraction, color)
local full = math.floor(width * math.max(0, math.min(1, fraction)) + 0.5)
term.set_cursor(x, y)
term.set_bg(color)
term.write(string.rep(" ", full))
term.set_bg(term.colors.gray)
term.write(string.rep(" ", width - full))
term.set_bg(term.colors.black)
end
-- one line of the table: name, count, bar
function M.row(y, name, count, max)
term.set_cursor(1, y)
term.set_fg(term.colors.white)
term.write(text.pad(name, 14) .. text.pad(text.thousands(count), 9))
local color = term.colors.lime
if count < max * 0.2 then
color = term.colors.red
end
M.bar(24, y, 26, count / max, color)
end
return Mlocal config = import "/config"
local ui = import "/lib/ui"
local vault = peripheral.find("inventory")
if vault == nil then
error("no inventory next to the computer or on its cable")
end
while true do
term.set_bg(term.colors.black)
term.clear()
term.set_cursor(1, 1)
term.set_fg(term.colors.yellow)
term.write(config.title)
local low = false
for i, item in ipairs(config.items) do
local count = vault.count(item.id)
ui.row(2 + i, item.name, count, item.max)
if count < item.max * 0.2 then
low = true
end
end
rs.set(config.alarm_side, low)
sleep(config.refresh)
endimport "/apps/stock/main"The main program says what to show; lib/ui knows how to draw it; config says which items. To watch a second vault on another computer, copy the four files and change only config.
Sharing code between computers
The files live on the medium, not in the computer. Three ways to bring a library or a program to another computer:
- Carry the medium. Sneak + right-click the computer with an empty hand to eject its disk, right-click another computer with it. Every file comes along. A computer reads the media of its own age and the older ones (Punch Card Deck, Magnetic Tape Reel, Floppy Disk, Solid-State Drive), see
The computers. - Copy and paste. Open the file in the editor, Ctrl+A then Ctrl+C; open a file on the other computer and paste with Ctrl+V. It is the simplest way into a Microcontroller, whose memory is soldered and takes no medium. It also works from this site: the Copy button of a code block, then Ctrl+V in the editor.
- Send it over the network. From the Minicomputer on, computers linked by Data Cable (or by radio, from the Personal Computer) can send each other text. A pair of tiny programs copies a file:
-- send <file> <computer id>: sends a file to another computer
local path, target = arg[1], tonumber(arg[2])
local text = assert(fs.read(path), "no file " .. tostring(path))
if net.send(target, {path = path, text = text}, "files") then
print("sent " .. #text .. " characters")
else
print("computer " .. tostring(target) .. " not reachable")
end-- receive: writes every file sent to this computer
print("waiting for files... (computer " .. os.id() .. ")")
while true do
local m = os.pull_event("message")
if m.channel == "files" and type(m.data) == "table" then
fs.write(m.data.path, m.data.text)
print("received " .. m.data.path .. " from " .. m.sender)
end
endRun receive on one computer, then send lib/text 12 on the other (12 being the id the receiver shows). A message carries up to 32768 characters; Networks explains the rest.
See also
import()andrequire(): the reference.Functions: local and global functions, and the rule about locals of enclosing functions.A project in several files: a larger project built from modules.fs: the files and folders themselves.