Create: Computing AgesDoc Brass
Bibliothèques

fs

Fichiers et dossiers du support de stockage : sauvegarder des données, tenir un journal, ranger ses programmes.

Tous les ordinateurs

La bibliothèque fs lit et écrit les fichiers du support de stockage de l'ordinateur : les mêmes fichiers que le shell affiche avec ls, ouvre avec edit et lance par leur nom. Un programme s'en sert pour garder ce qui doit survivre à un redémarrage ou à un rechargement du monde (un compteur, un record, des réglages), pour tenir un journal, ou pour ranger ses données dans des dossiers.

Brass
fs.write("notes", "Vérifier la Presse mécanique de la ligne 2")
print(fs.read("notes"))
print(fs.size("notes") .. " octets")
Écran
Vérifier la Presse mécanique de la ligne 2
44 octets

(42 caractères, mais 44 octets : chaque é en prend deux.)

Tous les ordinateurs ont la bibliothèque fs. Ce qu'elle peut contenir dépend du support :

supportcapacitélu par
Paquet de cartes perforées4 Ko (4 096 octets)tous les ordinateurs qui ont un lecteur
Bobine de bande magnétique16 KoOrdinateur à transistors, Mini-ordinateur, Micro-ordinateur, Ordinateur moderne
Disquette64 KoMini-ordinateur, Micro-ordinateur, Ordinateur moderne
SSD1 MoOrdinateur moderne
mémoire soudée16 Kole Microcontrôleur, qui n'a pas de lecteur

Les fichiers vivent sur le support. Insérez un support d'un clic droit, éjectez-le accroupi d'un clic droit (main vide), mettez-le dans un autre ordinateur : les fichiers le suivent. Le contenu est gardé sur le serveur, dans la sauvegarde du monde ; l'objet ne porte qu'un numéro. Le Microcontrôleur n'a pas de lecteur : sa mémoire est soudée, et reste avec lui même quand vous le ramassez.

Sans support, toutes les fonctions de fs sauf fs.cwd arrêtent le programme avec no storage medium. La section Motifs courants montre comment en attendre un.

Fonctions
fs.read(path)Lit un fichier entier et renvoie son texte.
fs.write(path, text)Remplace le contenu d'un fichier par text, ou le crée.
fs.append(path, text)Ajoute du texte à la fin d'un fichier, après ce qu'il contient déjà. Le fichier (et ses dossiers) est créé au besoin.
fs.exists(path)Dit si un fichier ou un dossier a ce chemin.
fs.is_dir(path)Dit si le chemin est un dossier.
fs.list([folder])Les noms des fichiers et dossiers qui se trouvent directement dans un dossier.
fs.make_dir(path)Crée un dossier, et les dossiers qui y mènent.
fs.delete(path)Supprime un fichier, ou un dossier avec tout ce qu'il contient.
fs.move(from, to)Déplace ou renomme un fichier ou un dossier (avec tout son contenu).
fs.copy(from, to)Copie un fichier, ou un dossier avec tout ce qu'il contient.
fs.cwd()Le dossier courant : celui d'où partent les chemins relatifs.
fs.size(path)La taille d'un fichier en octets, ou de tout ce que contient un dossier.
fs.free()Les octets encore libres sur le support.
fs.capacity()La taille totale du support en octets : 4096 pour un Paquet de cartes perforées, 16384 pour une Bobine de bande magnétique ou la mémoire du Microcontrôleur, 65536 pour une Disquette, 1048576 pour un SSD.

Chemins

Un chemin désigne un fichier ou un dossier : startup, journaux/porte, /jeux/snake/record.

  • Un nom est fait de lettres sans accents, de chiffres, de _, . et -, de 1 à 32 caractères. Pas d'espaces : fs.write("mon fichier", "...") s'arrête avec bad argument #1 to 'write' (invalid file name 'mon fichier'). Majuscules et minuscules sont différentes : Journal et journal sont deux fichiers.
  • / sépare les dossiers : journaux/2024/porte.
  • Un chemin est relatif au dossier courant (le dossier où était le shell quand le programme a été lancé, voir fs.cwd), ou part de la racine quand il commence par /.
  • . est le dossier lui-même et .. le dossier au-dessus. Remonter au-dessus de la racine s'arrête avec bad argument #1 to 'read' (no folder above the root).
  • Les barres obliques répétées ou finales ne comptent pas : journaux//porte/ est journaux/porte.
  • Un chemin complet fait 128 caractères au plus.

Un support contient au plus 1024 fichiers et dossiers, quelle que soit leur taille. Les dossiers ne prennent pas de place ; un fichier prend les octets de son texte (une lettre sans accent compte 1, une lettre accentuée 2).

Quand quelque chose ne va pas, le programme s'arrête avec l'un de ces messages (attrapez-les avec pcall quand cela peut arriver, comme un support plein) :

messagequand
no storage mediumle lecteur est vide
not enough spacele texte ne tient pas sur le support
too many files (1024 at most)le support contient déjà 1024 fichiers et dossiers
'journaux' is a folderécrire, déplacer ou copier sur un dossier
'sauve' is a file, not a folderun chemin traverse un fichier comme si c'était un dossier (sauve/score quand sauve est un fichier)
no folder 'journaux', 'journaux' is a filefs.list sur autre chose qu'un dossier
no file 'rapport'déplacer ou copier quelque chose qui n'existe pas
bad argument #1 to 'write' (invalid file name 'é')un nom avec un caractère interdit
bad argument #1 to 'write' (path too long (128 characters at most))un chemin de plus de 128 caractères

Les mêmes noms et chemins servent dans le shell, avec ls, cd, mkdir, rm, cp et mv (voir Le shell et l'éditeur).

Lire et écrire

Les fichiers contiennent du texte. Pour sauvegarder un nombre, changez-le en texte avec tostring (ou laissez fs.write le faire), et relisez-le avec tonumber. Pour sauvegarder une liste, mettez une entrée par ligne.

#

fs.read(path)

→ string|nil

Lit un fichier entier et renvoie son texte.

Paramètres
path string
le fichier à lire
Renvoie
string|nil
tout le texte du fichier, ou nil si ce fichier n'existe pas

Quand le fichier n'existe pas, ou quand le chemin est un dossier, le résultat est nil : pas d'erreur. « Lire, ou prendre une valeur par défaut » tient donc en une ligne :

Brass
local texte = fs.read("fichier_absent")
print(texte)
local record = tonumber(fs.read("meilleur_score") or "0") or 0
print("meilleur score : " .. record)
Écran
nil
meilleur score : 0

Le texte revient exactement tel qu'il a été écrit, retours à la ligne compris ; string.split(texte, "\n") le découpe en lignes. Lire coûte une instruction de plus par tranche de 16 caractères.

Voir aussi fs.write() fs.exists()

#

fs.write(path, text)

Remplace le contenu d'un fichier par text, ou le crée.

Paramètres
path string
le fichier à écrire
text string
le nouveau contenu (un nombre est changé en texte)

L'ancien contenu est perdu. Les dossiers du chemin sont créés au besoin : fs.write("sauve/snake/record", "120") marche sur un support vide.

Brass
fs.write("sauve/snake/record", 120)
print(fs.read("sauve/snake/record"))
print(fs.is_dir("sauve/snake"))
Écran
120
true

Les nombres sont acceptés et changés en texte ; toute autre valeur arrête le programme avec bad argument #2 to 'write' (string expected, got boolean) : utilisez tostring(valeur). L'écriture échoue quand un dossier porte le nom du fichier ('sauve' is a folder), quand le support est plein (not enough space) ou contient déjà 1024 entrées.

Le fichier est écrit tout de suite : un autre programme de l'ordinateur, ou le shell, le voit aussitôt. Le monde l'enregistre sur le disque avec tout le reste. Écrire coûte une instruction de plus par tranche de 16 caractères.

Voir aussi fs.append() fs.read()

#

fs.append(path, text)

Ajoute du texte à la fin d'un fichier, après ce qu'il contient déjà. Le fichier (et ses dossiers) est créé au besoin.

Paramètres
path string
le fichier à compléter
text string
le texte à ajouter à la fin (un nombre est changé en texte)

C'est l'outil des journaux : chaque évènement ajoute une ligne, les anciennes restent. Rien n'est ajouté entre deux appels : terminez chaque ligne par "\n" vous-même.

Brass
fs.append("journaux/gare", "train venant du nord\n")
fs.append("journaux/gare", "train venant de l'est\n")
write(fs.read("journaux/gare"))
Écran
train venant du nord
train venant de l'est

Un journal qui ne fait que grandir finira par remplir le support : voyez le journal tournant dans les Motifs courants. Ajouter coûte une instruction de plus par tranche de 16 caractères ajoutés.

Voir aussi fs.write()

Fichiers et dossiers

#

fs.exists(path)

→ boolean

Dit si un fichier ou un dossier a ce chemin.

Paramètres
path string
un fichier ou un dossier
Renvoie
boolean
true si un fichier ou un dossier a ce chemin
Brass
fs.write("reglages", "vitesse=64")
print(fs.exists("reglages"))
print(fs.exists("records"))
print(fs.exists("/"))
Écran
true
false
true

La racine (/) existe toujours. Pour distinguer un fichier d'un dossier, utilisez fs.is_dir. Pour lire un fichier qui peut manquer, fs.read suffit : il renvoie nil.

Voir aussi fs.is_dir() fs.read()

#

fs.is_dir(path)

→ boolean

Dit si le chemin est un dossier.

Paramètres
path string
un fichier ou un dossier
Renvoie
boolean
true pour un dossier, false pour un fichier ou pour rien
Brass
fs.write("jeux/snake/main", "-- le jeu")
print(fs.is_dir("jeux"))
print(fs.is_dir("jeux/snake/main"))
print(fs.is_dir("rien_ici"))
Écran
true
false
false

Voir aussi fs.exists() fs.list()

#

fs.list([folder])

→ table

Les noms des fichiers et dossiers qui se trouvent directement dans un dossier.

Paramètres
folder string facultatif
le dossier à lister ; le dossier courant s'il est omis
Renvoie
table
les noms des fichiers et dossiers qu'il contient, dans l'ordre alphabétique

Les noms viennent sans leur dossier (porte, pas journaux/porte), triés : les chiffres d'abord, puis les majuscules, puis les minuscules. Dossiers et fichiers sont mélangés : demandez à fs.is_dir de les distinguer.

Brass
fs.write("journaux/porte", "")
fs.write("journaux/Presse", "")
fs.write("journaux/2024/mars", "")
for _, nom in ipairs(fs.list("journaux")) do
  if fs.is_dir("journaux/" .. nom) then
    print(nom .. "/")
  else
    print(nom)
  end
end
Écran
2024/
Presse
porte

Un dossier vide donne une table vide. Un chemin qui n'est pas un dossier arrête le programme avec no folder 'journaux' (ou 'journaux' is a file). Pour parcourir aussi les dossiers des dossiers, voyez l'arborescence dans les Motifs courants.

Voir aussi fs.is_dir() fs.cwd()

#

fs.make_dir(path)

Crée un dossier, et les dossiers qui y mènent.

Paramètres
path string
le dossier à créer

Rien ne se passe quand le dossier existe déjà. Quand un fichier porte ce nom, le programme s'arrête avec 'journaux' is a file.

Brass
fs.make_dir("archives/2024")
print(fs.is_dir("archives"), fs.is_dir("archives/2024"))
Écran
true    true

Vous en aurez rarement besoin avant d'écrire, puisque fs.write crée lui-même les dossiers du chemin. Elle sert à préparer un dossier vide qu'un programme ou un joueur remplira plus tard. Un dossier vide ne prend pas de place, mais compte dans les 1024 entrées du support.

Voir aussi fs.write() fs.list()

#

fs.delete(path)

→ boolean

Supprime un fichier, ou un dossier avec tout ce qu'il contient.

Paramètres
path string
le fichier ou le dossier à supprimer
Renvoie
boolean
true si quelque chose a été supprimé, false si rien ne portait ce nom

Pas de confirmation, pas de corbeille : ce qui est supprimé est perdu. Un chemin absent n'est pas une erreur, le résultat est simplement false.

Brass
fs.write("tmp/rapport", "brouillon")
print(fs.delete("tmp"))
print(fs.delete("tmp"))
print(fs.exists("tmp/rapport"))
Écran
true
false
false

La racine elle-même ne peut pas être supprimée (the root cannot be deleted), et un dossier vidé reste en place (supprimez-le aussi si vous voulez qu'il disparaisse).

Voir aussi fs.exists()

#

fs.move(from, to)

Déplace ou renomme un fichier ou un dossier (avec tout son contenu).

Paramètres
from string
le fichier ou le dossier à déplacer
to string
son nouveau chemin, nom compris

to est le nouveau chemin complet, pas un dossier où ranger : pour mettre rapport dans le dossier archives, écrivez fs.move("rapport", "archives/rapport"). (La commande mv du shell est plus souple et range dans un dossier ; fs.move est stricte.)

Brass
fs.write("rapport", "64 lingots de fer")
fs.move("rapport", "archives/rapport")
print(fs.exists("rapport"), fs.read("archives/rapport"))
Écran
false   64 lingots de fer
  • Les dossiers du chemin to sont créés.
  • Quand un fichier a déjà le chemin to, il est remplacé.
  • Quand to est un dossier existant, le programme s'arrête avec 'archives' is a folder.
  • Déplacer quelque chose qui n'existe pas s'arrête avec no file 'rapport' ; déplacer un dossier dans lui-même, avec a folder cannot go inside itself.

Voir aussi fs.copy() fs.delete()

#

fs.copy(from, to)

Copie un fichier, ou un dossier avec tout ce qu'il contient.

Paramètres
from string
le fichier ou le dossier à copier
to string
le chemin de la copie, nom compris

Les règles pour to sont celles de fs.move : un chemin complet, des dossiers créés en route, un fichier remplacé, un dossier refusé. La copie demande de la place sur le support (not enough space), et compte dans les 1024 entrées.

Brass
fs.write("startup", 'print("Fonderie prête")')
fs.copy("startup", "secours/startup")
print(fs.read("secours/startup"))
Écran
print("Fonderie prête")

Copier un fichier sur lui-même s'arrête avec a file cannot be copied onto itself. Une copie coûte une instruction de plus par tranche de 16 octets copiés.

Voir aussi fs.move()

#

fs.cwd()

→ string

Le dossier courant : celui d'où partent les chemins relatifs.

Renvoie
string
le dossier courant, depuis la racine : "/" ou "/jeux/snake"

C'est le dossier où se trouvait le shell (cd jeux) quand le programme a été lancé, écrit depuis la racine avec un / au début. Un programme ne peut pas le changer. Elle marche même sans support.

Brass
print(fs.cwd())
Écran
/
Attention

Les chemins relatifs partent du dossier courant, pas du dossier du programme. Lancé depuis la racine avec jeux/snake/main, un programme qui appelle fs.read("record") lit /record, pas /jeux/snake/record. (import est différent : il part du dossier du fichier.) Pour garder des données à côté du programme, construisez le chemin à partir de arg[0], le chemin du fichier en cours :

Brass
local morceaux = string.split(arg[0], "/")  -- "jeux/snake/main" donne jeux, snake, main
table.remove(morceaux)                      -- retire le nom du fichier
local ici = "/" .. table.concat(morceaux, "/")
fs.write(ici .. "/record", "120")           -- toujours /jeux/snake/record

Voir aussi fs.list()

Place

#

fs.size(path)

→ number|nil

La taille d'un fichier en octets, ou de tout ce que contient un dossier.

Paramètres
path string
un fichier ou un dossier
Renvoie
number|nil
la taille en octets, ou nil s'il n'y a rien à ce chemin
Brass
fs.write("journaux/porte", "ouverte\nfermée\n")
fs.write("journaux/presse", "bloquée\n")
print(fs.size("journaux/porte"))
print(fs.size("journaux"))
print(fs.size("rien"))
Écran
16
25
nil

Un octet, c'est une lettre sans accent, un chiffre, une espace ou un retour à la ligne. Une lettre accentuée en prend 2 : fs.size peut donc dépasser #texte.

Voir aussi fs.free()

#

fs.free()

→ number

Les octets encore libres sur le support.

Renvoie
number
la place libre sur le support, en octets

Vérifiez-la avant d'écrire quelque chose de gros, ou pour prévenir avant que le support soit plein :

Brass
if fs.free() < 1000 then
  print("Disquette presque pleine : archivez les vieux journaux")
end

Voir aussi fs.capacity() fs.size()

#

fs.capacity()

→ number

La taille totale du support en octets : 4096 pour un Paquet de cartes perforées, 16384 pour une Bobine de bande magnétique ou la mémoire du Microcontrôleur, 65536 pour une Disquette, 1048576 pour un SSD.

Renvoie
number
la taille totale du support, en octets
Brass
fs.write("recettes", string.rep("plaque de fer = 1 lingot de fer\n", 40))
local pris = fs.capacity() - fs.free()
print(pris .. " octets pris sur " .. fs.capacity())
print(math.floor(pris * 100 / fs.capacity()) .. " % plein")
Écran
1280 octets pris sur 65536
1 % plein

(Sur une Disquette.)

Voir aussi fs.free()

Motifs courants

Un compteur qui survit aux redémarrages. Lisez la valeur au départ (0 quand le fichier n'existe pas encore), sauvegardez-la à chaque changement. Ici, les trains qui passent sur un rail détecteur à gauche :

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

La même idée dans une petite fonction, lancée trois fois comme si l'ordinateur avait redémarré deux fois :

Brass
local function incrementer(nom)
  local n = tonumber(fs.read(nom) or "0") or 0
  n = n + 1
  fs.write(nom, tostring(n))
  return n
end
print(incrementer("demarrages"))
print(incrementer("demarrages"))
print(incrementer("demarrages"))
Écran
1
2
3

Un record. Ne sauvegardez que si le nouveau score bat l'ancien :

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

Un journal tournant. Un journal ne doit pas remplir le support. Quand le fichier dépasserait une taille limite, il devient porte.old (à la place du précédent), et un nouveau journal commence. Le support ne contient jamais plus de deux fois la limite :

Brass
local JOURNAL = "journaux/porte"
local MAX = 60  -- octets ; prenez 2000 ou plus pour un vrai journal

local function noter(texte)
  local ligne = texte .. "\n"
  if (fs.size(JOURNAL) or 0) + #ligne > MAX then
    fs.move(JOURNAL, JOURNAL .. ".old")
  end
  fs.append(JOURNAL, ligne)
end

noter("ouverte par Steve")
noter("fermée")
noter("ouverte par Alex")
noter("fermée")
noter("ouverte par Steve")
for _, nom in ipairs(fs.list("journaux")) do
  print(nom, fs.size("journaux/" .. nom))
end
Écran
porte   18
porte.old   51

Des réglages dans un fichier. Un fichier de lignes clé=valeur que les joueurs modifient avec edit, sans toucher au programme. Les lignes qui commencent par # sont des commentaires ; les nombres reviennent en nombres :

Brass
fs.write("coffre.cfg", "# réglages du Coffre à objet renforcé de fer\nseuil = 256\nface=back\nnom = Coffre de fer\n")

local function lire_reglages(chemin)
  local reglages = {}
  local texte = fs.read(chemin)
  if not texte then
    return reglages
  end
  for _, ligne in ipairs(string.split(texte, "\n")) do
    ligne = string.trim(ligne)
    local egal = string.find(ligne, "=")
    if egal and not string.starts(ligne, "#") then
      local cle = string.trim(string.sub(ligne, 1, egal - 1))
      local valeur = string.trim(string.sub(ligne, egal + 1))
      reglages[cle] = tonumber(valeur) or valeur
    end
  end
  return reglages
end

local reglages = lire_reglages("coffre.cfg")
print(reglages.nom, reglages.seuil + 1, reglages.face)
Écran
Coffre de fer   257 back

L'arborescence d'un support. Une fonction qui liste un dossier, et s'appelle elle-même pour chaque dossier qu'il contient :

Brass
fs.write("startup", "print('bonjour')")
fs.write("jeux/snake/main", "-- le jeu")
fs.write("jeux/snake/record", "120")
fs.write("journaux/porte", "ouverte\nfermée\n")
fs.make_dir("secours")

local function joindre(dossier, nom)
  if dossier == "/" then
    return "/" .. nom
  end
  return dossier .. "/" .. nom
end

local function arbre(dossier, retrait)
  for _, nom in ipairs(fs.list(dossier)) do
    local chemin = joindre(dossier, nom)
    if fs.is_dir(chemin) then
      print(retrait .. nom .. "/")
      arbre(chemin, retrait .. "  ")
    else
      print(retrait .. nom .. "  " .. fs.size(chemin) .. " o")
    end
  end
end

arbre("/", "")
Écran
jeux/
  snake/
    main  9 o
    record  3 o
journaux/
  porte  16 o
secours/
startup  16 o

Attendre un support. Un programme lancé continue quand on éjecte son support, mais ses appels à fs échouent. Un programme qui demande au joueur de changer de support (pour copier ses journaux sur une autre disquette, par exemple) attend l'évènement disk, et essaie fs avec pcall, qui attrape l'erreur au lieu d'arrêter le programme :

Brass
while not pcall(fs.free).ok do
  print("Insérez une disquette")
  os.pull_event("disk")
end
print("Support prêt : " .. fs.free() .. " octets libres")

Pour organiser un programme plus gros en plusieurs fichiers, voyez Un projet en plusieurs fichiers et import ; pour un enregistreur complet, Enregistreur de données et graphique.