
fs
Files and folders of the storage medium: save data, keep logs, organise programs.
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.
fs.write("notes", "Check the Mechanical Press on line 2")
print(fs.read("notes"))
print(fs.size("notes") .. " bytes")Check the Mechanical Press on line 2 36 bytes
Every computer has the fs library. What it can hold depends on the medium:
| medium | capacity | read by |
|---|---|---|
| Punch Card Deck | 4 KB (4,096 bytes) | every computer with a drive |
| Magnetic Tape Reel | 16 KB | Transistor Mainframe, Minicomputer, Personal Computer, Modern Computer |
| Floppy Disk | 64 KB | Minicomputer, Personal Computer, Modern Computer |
| Solid-State Drive | 1 MB | Modern Computer |
| soldered memory | 16 KB | the 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.
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 withbad argument #1 to 'write' (invalid file name 'my file'). Upper and lower case are different:Logandlogare 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 withbad argument #1 to 'read' (no folder above the root).- Repeated or final slashes do not matter:
logs//door/islogs/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):
| message | when |
|---|---|
no storage medium | the drive is empty |
not enough space | the text does not fit on the medium |
too many files (1024 at most) | the medium already holds 1024 files and folders |
'logs' is a folder | writing, moving or copying onto a folder |
'save' is a file, not a folder | a path goes through a file as if it were a folder (save/score when save is a file) |
no folder 'logs', 'logs' is a file | fs.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.
Reads a whole file and returns its text.
pathstring- the file to read
- string|nil
- the whole text of the file, or
nilif 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:
local text = fs.read("missing_file")
print(text)
local best = tonumber(fs.read("best_score") or "0") or 0
print("best score: " .. best)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.
pathstring- the file to write
textstring- 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.
fs.write("save/snake/best", 120)
print(fs.read("save/snake/best"))
print(fs.is_dir("save/snake"))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.
pathstring- the file to add to
textstring- 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:
fs.append("logs/gate", "train from the north\n")
fs.append("logs/gate", "train from the east\n")
write(fs.read("logs/gate"))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
Tells whether a file or a folder has that path.
pathstring- a file or a folder
- boolean
trueif a file or a folder has that path
fs.write("settings", "speed=64")
print(fs.exists("settings"))
print(fs.exists("high_scores"))
print(fs.exists("/"))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()
Tells whether the path is a folder.
pathstring- a file or a folder
- boolean
truefor a folder,falsefor a file or nothing
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"))true false false
See also fs.exists() fs.list()
The names of the files and folders directly inside a folder.
folderstring optional- the folder to list; the current folder when left out
- 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.
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
end2024/ 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.
pathstring- 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.
fs.make_dir("archive/2024")
print(fs.is_dir("archive"), fs.is_dir("archive/2024"))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()
Deletes a file, or a folder with everything inside it.
pathstring- the file or the folder to delete
- boolean
trueif something was deleted,falseif 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.
fs.write("tmp/report", "draft")
print(fs.delete("tmp"))
print(fs.delete("tmp"))
print(fs.exists("tmp/report"))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).
fromstring- the file or folder to move
tostring- 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.)
fs.write("report", "64 iron ingots")
fs.move("report", "archive/report")
print(fs.exists("report"), fs.read("archive/report"))false 64 iron ingots
- The folders on the way to
toare created. - When a file already has the path
to, it is replaced. - When
tois 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, witha 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.
fromstring- the file or folder to copy
tostring- 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.
fs.write("startup", 'print("Smelter ready")')
fs.copy("startup", "backup/startup")
print(fs.read("backup/startup"))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()
The current folder: the folder relative paths start from.
- 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.
print(fs.cwd())/
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:
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/bestSee also fs.list()
Space
The size of a file in bytes, or of everything inside a folder.
pathstring- a file or a folder
- number|nil
- the size in bytes, or
nilif there is nothing at that path
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"))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()
The bytes still free on the medium.
- number
- the free space on the medium, in bytes
Check it before writing something big, or to warn before the medium is full:
if fs.free() < 1000 then
print("Floppy almost full: archive the old logs")
endSee also fs.capacity() fs.size()
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.
- number
- the total size of the medium, in bytes
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")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:
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
endThe same idea in a small function, run three times as if the computer had rebooted twice:
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"))1 2 3
A high score. Save only when the new score beats the old one:
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"))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:
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))
enddoor 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:
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)Iron vault 257 back
The tree of a medium. A function that lists a folder, and calls itself for each folder inside:
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("/", "")backup/
games/
snake/
best 3 B
main 11 B
logs/
door 14 B
startup 14 BWait 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:
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.