Create: Computing AgesBrass Docs
Libraries

fs

Files and folders of the storage medium: save data, keep logs, organise programs.

All computers

The fs library reads and writes the files of the storage medium in the computer: the same files that the shell shows with ls, opens with edit and runs by their name. A program uses it to keep what must survive a reboot or a world reload (a counter, a high score, settings), to write a log, or to keep its data in order in folders.

Brass
fs.write("notes", "Check the Mechanical Press on line 2")
print(fs.read("notes"))
print(fs.size("notes") .. " bytes")
Screen
Check the Mechanical Press on line 2
36 bytes

Every computer has the fs library. What it can hold depends on the medium:

mediumcapacityread by
Punch Card Deck4 KB (4,096 bytes)every computer with a drive
Magnetic Tape Reel16 KBTransistor Mainframe, Minicomputer, Personal Computer, Modern Computer
Floppy Disk64 KBMinicomputer, Personal Computer, Modern Computer
Solid-State Drive1 MBModern Computer
soldered memory16 KBthe Microcontroller, which has no drive

The files live on the medium. Insert a medium with a right-click, eject it with sneak and right-click (empty hand), put it in another computer: the files follow it. The contents are kept on the server, in the world's save; the item only carries a number. The Microcontroller has no drive: its memory is soldered, and stays with it even when you pick it up.

Without a medium, every function of fs except fs.cwd stops the program with no storage medium. The section Common patterns shows how to wait for one.

Functions
fs.read(path)Reads a whole file and returns its text.
fs.write(path, text)Replaces the contents of a file with text, or creates it.
fs.append(path, text)Adds text at the end of a file, after what it already contains. The file (and its folders) is created if needed.
fs.exists(path)Tells whether a file or a folder has that path.
fs.is_dir(path)Tells whether the path is a folder.
fs.list([folder])The names of the files and folders directly inside a folder.
fs.make_dir(path)Creates a folder, and the folders that lead to it.
fs.delete(path)Deletes a file, or a folder with everything inside it.
fs.move(from, to)Moves or renames a file or a folder (with all its contents).
fs.copy(from, to)Copies a file, or a folder with everything inside it.
fs.cwd()The current folder: the folder relative paths start from.
fs.size(path)The size of a file in bytes, or of everything inside a folder.
fs.free()The bytes still free on the medium.
fs.capacity()The total size of the medium in bytes: 4096 for a Punch Card Deck, 16384 for a Magnetic Tape Reel or the memory of the Microcontroller, 65536 for a Floppy Disk, 1048576 for a Solid-State Drive.

Paths

A path names a file or a folder: startup, logs/door, /games/snake/best.

  • A name is made of letters without accents, digits, _, . and -, from 1 to 32 characters. No spaces: fs.write("my file", "...") stops with bad argument #1 to 'write' (invalid file name 'my file'). Upper and lower case are different: Log and log are two files.
  • / separates the folders: logs/2024/door.
  • A path is relative to the current folder (the folder of the shell when the program was started, see fs.cwd), or starts from the root when it begins with /.
  • . is the folder itself and .. the folder above it. Going above the root stops with bad argument #1 to 'read' (no folder above the root).
  • Repeated or final slashes do not matter: logs//door/ is logs/door.
  • A whole path has 128 characters at most.

A medium holds at most 1024 files and folders, whatever their size. Folders take no room; a file takes the bytes of its text (letters without accents count 1, an accented letter 2).

When something goes wrong, the program stops with one of these messages (catch them with pcall when it can happen, like a full medium):

messagewhen
no storage mediumthe drive is empty
not enough spacethe text does not fit on the medium
too many files (1024 at most)the medium already holds 1024 files and folders
'logs' is a folderwriting, moving or copying onto a folder
'save' is a file, not a foldera path goes through a file as if it were a folder (save/score when save is a file)
no folder 'logs', 'logs' is a filefs.list on something that is not a folder
no file 'report'moving or copying something that does not exist
bad argument #1 to 'write' (invalid file name 'é')a name with a forbidden character
bad argument #1 to 'write' (path too long (128 characters at most))a path longer than 128 characters

The same names and paths work in the shell, with ls, cd, mkdir, rm, cp and mv (see The shell and the editor).

Reading and writing

Files contain text. To save a number, turn it into text with tostring (or let fs.write do it), and read it back with tonumber. To save a list, put one entry per line.

#

fs.read(path)

→ string|nil

Reads a whole file and returns its text.

Parameters
path string
the file to read
Returns
string|nil
the whole text of the file, or nil if there is no such file

When the file does not exist, or when the path is a folder, the result is nil: no error. That makes "read, or take a default value" a one-liner:

Brass
local text = fs.read("missing_file")
print(text)
local best = tonumber(fs.read("best_score") or "0") or 0
print("best score: " .. best)
Screen
nil
best score: 0

The text comes back exactly as written, line breaks included; string.split(text, "\n") cuts it into lines. Reading costs one extra instruction per 16 characters.

See also fs.write() fs.exists()

#

fs.write(path, text)

Replaces the contents of a file with text, or creates it.

Parameters
path string
the file to write
text string
the new contents (a number is turned into text)

The old contents are lost. The folders on the way are created if needed, so fs.write("save/snake/best", "120") works on an empty medium.

Brass
fs.write("save/snake/best", 120)
print(fs.read("save/snake/best"))
print(fs.is_dir("save/snake"))
Screen
120
true

Numbers are accepted and turned into text; any other value stops the program with bad argument #2 to 'write' (string expected, got boolean): use tostring(value). Writing fails when a folder has the name of the file ('save' is a folder), when the medium is full (not enough space) or holds 1024 entries already.

The file is written at once: another program of the computer, or the shell, sees it right away. The world saves it to disk with everything else. Writing costs one extra instruction per 16 characters.

See also fs.append() fs.read()

#

fs.append(path, text)

Adds text at the end of a file, after what it already contains. The file (and its folders) is created if needed.

Parameters
path string
the file to add to
text string
the text to add at the end (a number is turned into text)

This is the tool for logs: each event adds one line, the older lines stay. Nothing is added between two calls, so end each line with "\n" yourself:

Brass
fs.append("logs/gate", "train from the north\n")
fs.append("logs/gate", "train from the east\n")
write(fs.read("logs/gate"))
Screen
train from the north
train from the east

A log that only grows will fill the medium one day: see the log with rotation in Common patterns. Appending costs one extra instruction per 16 characters added.

See also fs.write()

Files and folders

#

fs.exists(path)

→ boolean

Tells whether a file or a folder has that path.

Parameters
path string
a file or a folder
Returns
boolean
true if a file or a folder has that path
Brass
fs.write("settings", "speed=64")
print(fs.exists("settings"))
print(fs.exists("high_scores"))
print(fs.exists("/"))
Screen
true
false
true

The root (/) always exists. To make a difference between a file and a folder, use fs.is_dir. To read a file that may be missing, fs.read alone is enough: it returns nil.

See also fs.is_dir() fs.read()

#

fs.is_dir(path)

→ boolean

Tells whether the path is a folder.

Parameters
path string
a file or a folder
Returns
boolean
true for a folder, false for a file or nothing
Brass
fs.write("games/snake/main", "-- the game")
print(fs.is_dir("games"))
print(fs.is_dir("games/snake/main"))
print(fs.is_dir("nothing_here"))
Screen
true
false
false

See also fs.exists() fs.list()

#

fs.list([folder])

→ table

The names of the files and folders directly inside a folder.

Parameters
folder string optional
the folder to list; the current folder when left out
Returns
table
the names of the files and folders it contains, in alphabetical order

The names come without their folder (door, not logs/door), sorted: digits first, then capitals, then small letters. Folders and files are mixed: ask fs.is_dir to tell them apart.

Brass
fs.write("logs/door", "")
fs.write("logs/Press", "")
fs.write("logs/2024/march", "")
for _, name in ipairs(fs.list("logs")) do
  if fs.is_dir("logs/" .. name) then
    print(name .. "/")
  else
    print(name)
  end
end
Screen
2024/
Press
door

An empty folder gives an empty table. A path that is not a folder stops the program with no folder 'logs' (or 'logs' is a file). To go through the folders inside the folders too, see the tree in Common patterns.

See also fs.is_dir() fs.cwd()

#

fs.make_dir(path)

Creates a folder, and the folders that lead to it.

Parameters
path string
the folder to create

Nothing happens when the folder already exists. When a file has that name, the program stops with 'logs' is a file.

Brass
fs.make_dir("archive/2024")
print(fs.is_dir("archive"), fs.is_dir("archive/2024"))
Screen
true    true

You seldom need it before writing, since fs.write creates the folders of the path itself. It is useful to prepare an empty folder that a program or a player will fill later. An empty folder takes no room, but counts in the 1024 entries of the medium.

See also fs.write() fs.list()

#

fs.delete(path)

→ boolean

Deletes a file, or a folder with everything inside it.

Parameters
path string
the file or the folder to delete
Returns
boolean
true if something was deleted, false if nothing had that name

There is no confirmation and no recycle bin: what is deleted is gone. A missing path is not an error, the result is just false.

Brass
fs.write("tmp/report", "draft")
print(fs.delete("tmp"))
print(fs.delete("tmp"))
print(fs.exists("tmp/report"))
Screen
true
false
false

The root itself cannot be deleted (the root cannot be deleted), and a folder made empty stays (delete it too if you want it gone).

See also fs.exists()

#

fs.move(from, to)

Moves or renames a file or a folder (with all its contents).

Parameters
from string
the file or folder to move
to string
its new path, name included

to is the full new path, not a folder to move into: to put report in the folder archive, write fs.move("report", "archive/report"). (The shell command mv is friendlier and moves into a folder; fs.move is strict.)

Brass
fs.write("report", "64 iron ingots")
fs.move("report", "archive/report")
print(fs.exists("report"), fs.read("archive/report"))
Screen
false   64 iron ingots
  • The folders on the way to to are created.
  • When a file already has the path to, it is replaced.
  • When to is an existing folder, the program stops with 'archive' is a folder.
  • Moving something that does not exist stops with no file 'report'; moving a folder inside itself, with a folder cannot go inside itself.

See also fs.copy() fs.delete()

#

fs.copy(from, to)

Copies a file, or a folder with everything inside it.

Parameters
from string
the file or folder to copy
to string
the path of the copy, name included

The rules for to are those of fs.move: a full path, folders created on the way, a file at to replaced, a folder at to refused. The copy needs room on the medium (not enough space), and counts in the 1024 entries.

Brass
fs.write("startup", 'print("Smelter ready")')
fs.copy("startup", "backup/startup")
print(fs.read("backup/startup"))
Screen
print("Smelter ready")

Copying a file onto itself stops with a file cannot be copied onto itself. A copy costs one extra instruction per 16 bytes copied.

See also fs.move()

#

fs.cwd()

→ string

The current folder: the folder relative paths start from.

Returns
string
the current folder, from the root: "/" or "/games/snake"

It is the folder where the shell was (cd games) when the program was started, written from the root with a leading /. A program cannot change it. It works even without a medium.

Brass
print(fs.cwd())
Screen
/
Watch out

Relative paths start from the current folder, not from the folder of the program. Started from the root with games/snake/main, a program that calls fs.read("best") reads /best, not /games/snake/best. (import is different: it starts from the folder of the file.) To keep data next to the program, build the path from arg[0], the path of the running file:

Brass
local parts = string.split(arg[0], "/")  -- "games/snake/main" gives games, snake, main
table.remove(parts)                      -- drop the file name
local here = "/" .. table.concat(parts, "/")
fs.write(here .. "/best", "120")         -- always /games/snake/best

See also fs.list()

Space

#

fs.size(path)

→ number|nil

The size of a file in bytes, or of everything inside a folder.

Parameters
path string
a file or a folder
Returns
number|nil
the size in bytes, or nil if there is nothing at that path
Brass
fs.write("logs/door", "opened\nclosed\n")
fs.write("logs/press", "jammed\n")
print(fs.size("logs/door"))
print(fs.size("logs"))
print(fs.size("nothing"))
Screen
14
21
nil

A byte is a letter without accent, a digit, a space or a line break. An accented letter takes 2 bytes, so fs.size can be larger than #text.

See also fs.free()

#

fs.free()

→ number

The bytes still free on the medium.

Returns
number
the free space on the medium, in bytes

Check it before writing something big, or to warn before the medium is full:

Brass
if fs.free() < 1000 then
  print("Floppy almost full: archive the old logs")
end

See also fs.capacity() fs.size()

#

fs.capacity()

→ number

The total size of the medium in bytes: 4096 for a Punch Card Deck, 16384 for a Magnetic Tape Reel or the memory of the Microcontroller, 65536 for a Floppy Disk, 1048576 for a Solid-State Drive.

Returns
number
the total size of the medium, in bytes
Brass
fs.write("recipes", string.rep("iron sheet = 1 iron ingot\n", 40))
local used = fs.capacity() - fs.free()
print(used .. " of " .. fs.capacity() .. " bytes used")
print(math.floor(used * 100 / fs.capacity()) .. " % full")
Screen
1040 of 65536 bytes used
1 % full

(Run on a Floppy Disk.)

See also fs.free()

Common patterns

A counter that survives reboots. Read the value at start (0 when there is no file yet), save it at each change. Here, the trains that pass over a detector rail on the left:

startup
local count = tonumber(fs.read("trains") or "0") or 0
while true do
  os.pull_event("redstone")
  if rs.get("left") > 0 then
    count = count + 1
    fs.write("trains", tostring(count))
    print("trains so far: " .. count)
  end
end

The same idea in a small function, run three times as if the computer had rebooted twice:

Brass
local function bump(name)
  local n = tonumber(fs.read(name) or "0") or 0
  n = n + 1
  fs.write(name, tostring(n))
  return n
end
print(bump("boots"))
print(bump("boots"))
print(bump("boots"))
Screen
1
2
3

A high score. Save only when the new score beats the old one:

Brass
local function save_best(score)
  local best = tonumber(fs.read("snake/best") or "0") or 0
  if score > best then
    fs.write("snake/best", tostring(score))
    return true
  end
  return false
end
print(save_best(120))
print(save_best(80))
print(fs.read("snake/best"))
Screen
true
false
120

A log with rotation. A log must not fill the medium. When the file would pass a size limit, it becomes door.old (replacing the previous one), and a new log starts. The medium never holds more than twice the limit:

Brass
local LOG = "logs/door"
local MAX = 60  -- bytes; take 2000 or more for a real log

local function log(text)
  local line = text .. "\n"
  if (fs.size(LOG) or 0) + #line > MAX then
    fs.move(LOG, LOG .. ".old")
  end
  fs.append(LOG, line)
end

log("opened by Steve")
log("closed")
log("opened by Alex")
log("closed")
log("opened by Steve")
for _, name in ipairs(fs.list("logs")) do
  print(name, fs.size("logs/" .. name))
end
Screen
door    16
door.old    45

Settings in a file. A file of key=value lines that players can change with edit, without touching the program. Lines starting with # are comments; numbers come back as numbers:

Brass
fs.write("vault.cfg", "# settings of the iron vault\nthreshold = 256\nside=back\nname = Iron vault\n")

local function load_config(path)
  local config = {}
  local text = fs.read(path)
  if not text then
    return config
  end
  for _, line in ipairs(string.split(text, "\n")) do
    line = string.trim(line)
    local eq = string.find(line, "=")
    if eq and not string.starts(line, "#") then
      local key = string.trim(string.sub(line, 1, eq - 1))
      local value = string.trim(string.sub(line, eq + 1))
      config[key] = tonumber(value) or value
    end
  end
  return config
end

local config = load_config("vault.cfg")
print(config.name, config.threshold + 1, config.side)
Screen
Iron vault  257 back

The tree of a medium. A function that lists a folder, and calls itself for each folder inside:

Brass
fs.write("startup", "print('hello')")
fs.write("games/snake/main", "-- the game")
fs.write("games/snake/best", "120")
fs.write("logs/door", "opened\nclosed\n")
fs.make_dir("backup")

local function join(folder, name)
  if folder == "/" then
    return "/" .. name
  end
  return folder .. "/" .. name
end

local function tree(folder, indent)
  for _, name in ipairs(fs.list(folder)) do
    local path = join(folder, name)
    if fs.is_dir(path) then
      print(indent .. name .. "/")
      tree(path, indent .. "  ")
    else
      print(indent .. name .. "  " .. fs.size(path) .. " B")
    end
  end
end

tree("/", "")
Screen
backup/
games/
  snake/
    best  3 B
    main  11 B
logs/
  door  14 B
startup  14 B

Wait for a medium. A running program goes on when its medium is ejected, but its fs calls fail. A program that asks the player to swap media (to copy its logs onto another floppy, for example) waits for the disk event, and tries fs with pcall, which catches the error instead of stopping the program:

Brass
while not pcall(fs.free).ok do
  print("Insert a floppy disk")
  os.pull_event("disk")
end
print("Medium ready: " .. fs.free() .. " bytes free")

To organise a bigger program in several files, see A project in several files and import; for a complete logger, Data logger and graph.