Programmes en plusieurs fichiers
Découper un programme en modules avec import, ranger un projet en dossiers, le lancer depuis startup, et partager du code entre ordinateurs.
Un programme qui pilote toute une usine atteint vite des centaines de lignes : lire les coffres à objet renforcés, dessiner l'écran, parler aux autres ordinateurs. Passé une certaine taille, un seul fichier devient difficile à lire et à modifier. Brass permet de le découper : chaque fichier fait un travail, et import rassemble les morceaux. Ces mêmes morceaux servent ensuite à vos autres programmes : écrivez une barre de progression une fois, utilisez-la partout.
Un premier module
Un module est un fichier ordinaire du support qui se termine par return et une valeur, presque toujours une table de fonctions. Voici une petite bibliothèque d'outils pour le texte, enregistrée sous lib/texte (edit lib/texte crée le dossier et le fichier) :
local M = {}
-- "Fer" -> "Fer " (coupé ou complété par des espaces jusqu'à la largeur)
function M.caler(texte, largeur)
texte = tostring(texte)
if #texte >= largeur then
return texte:sub(1, largeur)
end
return texte .. string.rep(" ", largeur - #texte)
end
-- 1234567 -> "1 234 567"
function M.milliers(n)
if n < 0 then
return "-" .. M.milliers(-n)
end
local chiffres = tostring(math.floor(n))
local resultat = ""
while #chiffres > 3 do
resultat = " " .. chiffres:sub(-3) .. resultat
chiffres = chiffres:sub(1, -4)
end
return chiffres .. resultat
end
return MUn programme s'en sert avec import, qui exécute le fichier et renvoie la table qu'il renvoie :
local texte = import "lib/texte"
print(texte.caler("Lingot de fer", 14) .. texte.milliers(1234567))
print(texte.caler("Or", 14) .. texte.milliers(87))Les noms sont à vous : texte pourrait s'appeler t ou txt. Par habitude, un module construit une table nommée M (pour module) et la renvoie à la fin.
import et require
import(chemin) exécute le fichier situé à chemin et renvoie ce que ce fichier renvoie. require est la même fonction, sous le nom que connaissent les joueurs de ComputerCraft. Comme un appel avec une seule chaîne n'a pas besoin de parenthèses, la forme habituelle est :
local texte = import "lib/texte"Seul sur sa ligne, import "lib/preparation" exécute un fichier et ignore ce qu'il renvoie : pratique pour un fichier qui ne fait que définir des fonctions globales ou préparer l'écran.
Un fichier sans return donne nil. Un module peut renvoyer n'importe quoi (un nombre, une chaîne, une fonction), mais une table est ce qu'il vous faut dans presque tous les cas.
Où le fichier est cherché
Le chemin part **du dossier du fichier qui appelle import**, pas du dossier où vous avez tapé la commande. Un fichier de lib qui importe "texte" obtient lib/texte.
| chemin | depuis apps/stock/main, désigne |
|---|---|
"ecran" | apps/stock/ecran (même dossier) |
"parties/barres" | apps/stock/parties/barres |
"../porte/main" | apps/porte/main (.. remonte d'un dossier) |
"/lib/texte" | lib/texte (un / au début part de la racine du support) |
Les lignes tapées à l'invite brass importent depuis le dossier courant (celui choisi avec cd). Les noms de fichier n'ont pas d'extension : import "lib/texte", pas "lib/texte.lua".
Quand le fichier ne peut pas être chargé, le programme s'arrête avec une erreur qui cite le chemin tel que vous l'avez écrit :
| message | sens |
|---|---|
cannot import 'lib/txt': no such file | aucun fichier à ce chemin (vérifiez l'orthographe, et le dossier du fichier qui importe) |
cannot import 'lib': it is a folder | le chemin désigne un dossier |
cannot import '../../x': no folder above the root | trop de .. |
cannot import 'ma lib': invalid file name 'ma lib' | les noms n'utilisent que des lettres, des chiffres, _, . et - |
no storage medium | l'ordinateur n'a pas de support |
Un module s'exécute une fois par programme
Le premier import d'un fichier l'exécute. Tous les import suivants du même fichier, depuis n'importe quel fichier du programme, renvoient la même valeur sans l'exécuter de nouveau. Pour le voir d'un coup, ce test écrit un tout petit module avec fs.write, puis l'importe deux fois :
fs.write("lib/compteur", [[
print("lib/compteur s'exécute")
local M = {total = 0}
function M.ajouter()
M.total = M.total + 1
end
return M
]])
local a = import "lib/compteur"
local b = import "lib/compteur"
a.ajouter()
b.ajouter()
print(a == b, a.total)lib/compteur s'exécute true 2
Le fichier s'est exécuté une fois, et a et b sont la même table : un module est un bon endroit pour un état partagé par tous les fichiers d'un programme (un cache de mesures, les réglages en cours).
Le souvenir de ce qui a été importé dure jusqu'à la fin du programme. Le lancement suivant importe tout à nouveau : après avoir modifié un module, relancez simplement le programme, pas besoin de redémarrer. À l'invite brass, chaque ligne est son propre petit programme, donc un import y exécute le fichier à chaque fois.
Ce qu'un module partage, et ce qu'il garde
- Les **variables et fonctions
local** au niveau du fichier d'un module sont à lui. Aucun autre fichier ne les voit, même avec le même nom : deux modules peuvent avoir chacun leurlocal function dessiner()sans problème. - Les variables et fonctions globales (déclarées sans
local) sont partagées par tous les fichiers du programme.
fs.write("lib/partage", [[
local secret = "seulement dans lib/partage"
nom_ferme = "Ferme à fer"
function crier(message)
print(message:upper())
end
]])
import "lib/partage"
print(secret)
print(nom_ferme)
crier("bonjour")nil Ferme à fer BONJOUR
Gardez tout en local et renvoyez dans la table du module ce dont les autres fichiers ont besoin. Les globales semblent pratiques, mais deux modules qui choisissent le même nom global écrasent la valeur l'un de l'autre, et rien ne vous prévient.
Dans un module, les fonctions peuvent utiliser les locales du niveau du fichier (comme M plus haut) : la règle qui interdit à une fonction d'utiliser les locales d'une fonction englobante ne s'applique pas au niveau du fichier (voir Fonctions).
Les imports circulaires
Si lib/a importe lib/b pendant que lib/b importe lib/a, aucun des deux ne peut finir en premier. Brass arrête le programme au lieu de tourner en rond pour toujours :
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
Le remède est de casser le cercle : déplacez ce dont les deux fichiers ont besoin dans un troisième module qui n'importe aucun des deux, ou passez la valeur en paramètre d'une fonction au lieu de l'importer.
Les erreurs dans un module
Une erreur dans un module porte le nom et la ligne du module lui-même, pour que vous sachiez quel fichier ouvrir :
fs.write("lib/casse", "local x = \nreturn x")
import "lib/casse"lib/casse:2: unexpected symbol near 'return'
Une erreur de compilation dans un module apparaît au moment où il est importé, pas au démarrage du programme principal : tout ce qui précède la ligne import s'est déjà exécuté. Un module dont l'exécution a échoué est exécuté de nouveau au prochain import.
Un module avec des réglages
Un fichier de réglages écrit en Brass
Un fichier qui renvoie simplement une table est une façon claire et sûre de séparer les réglages d'un programme de son code. Un joueur peut changer les réglages sans lire le programme, et les commentaires expliquent chaque valeur :
return {
titre = "STOCK DE LA FERME À FER",
intervalle = 5, -- secondes entre deux lectures
cote_alarme = "top", -- lampe allumée quand un stock est bas
objets = {
{nom = "Lingot de fer", id = "minecraft:iron_ingot", max = 2000},
{nom = "Pépite de fer", id = "minecraft:iron_nugget", max = 1000},
{nom = "Fer concassé", id = "create:crushed_raw_iron", max = 500},
},
}local config = import "/config"
print(config.titre)Contrairement à un fichier texte cle = valeur (voir Travailler avec du texte), celui-ci peut contenir des listes et des tables dans des tables, et n'a besoin d'aucun lecteur. Le prix : une faute de frappe y est une erreur de compilation, signalée config:5: ....
Des valeurs par défaut que le programme peut changer
Puisque chaque fichier reçoit la même table, un module peut proposer des valeurs par défaut que le programme principal change une fois au démarrage :
local M = {cote = "top", niveau = 15}
function M.sonner()
rs.set(M.cote, M.niveau)
end
function M.arreter()
rs.set(M.cote, 0)
end
return Mlocal alarme = import "/lib/alarme"
alarme.cote = "back" -- cet ordinateur a sa lampe à l'arrière
alarme.sonner()Plusieurs objets à partir d'un module
Quand il vous faut plusieurs exemplaires d'une même chose (trois lampes, deux presses), le module fournit une fonction qui construit une nouvelle table pour chacun. En Lua, on garderait les données de chaque objet dans une fermeture (closure) ; les fonctions de Brass ne peuvent pas capturer les locales d'une autre fonction, alors les données vont dans la table, et les fonctions reçoivent la table en self :
local Lampe = {}
local function allumer(self)
rs.set(self.cote, self.niveau)
self.allumee = true
end
local function eteindre(self)
rs.set(self.cote, 0)
self.allumee = false
end
local function basculer(self)
if self.allumee then
eteindre(self)
else
allumer(self)
end
end
function Lampe.nouvelle(cote, niveau)
return {cote = cote, niveau = niveau or 15, allumee = false, allumer = allumer, eteindre = eteindre, basculer = basculer}
end
return Lampelocal Lampe = import "/lib/lampe"
local alarme = Lampe.nouvelle("top")
local balise = Lampe.nouvelle("back", 7)
alarme:allumer()
balise:basculer()alarme:allumer() est un raccourci pour alarme.allumer(alarme) : les deux-points passent la table en premier argument, self. C'est pour vos propres objets. Les appareils que renvoie peripheral.wrap sont différents : leurs fonctions s'appellent avec un point, lampe.set(15) (voir Périphériques).
Ranger un projet en dossiers
Une organisation qui grandit bien : les bibliothèques partagées dans lib, un dossier par programme dans apps, les réglages à la racine, et un tout petit startup.
/startup lance le programme de cet ordinateur
/config les réglages de cet ordinateur
/lib/texte outils pour le texte (caler, milliers)
/lib/ui outils de dessin (barres, lignes)
/apps/stock/main l'écran des stocks
/apps/porte/main le contrôle de la porteLes commandes du shell comprennent les chemins et les dossiers :
> mkdir lib > edit lib/texte > edit apps/stock > cd apps/stock /apps/stock> main
edit apps/stock ouvre l'éditeur avec l'arborescence du dossier à gauche : cliquez sur un fichier pour l'ouvrir, plusieurs restent ouverts côte à côte. Le shell et l'éditeur donne les détails.
Quelques habitudes aident :
- Importez les bibliothèques partagées avec un chemin depuis la racine,
import "/lib/texte": la ligne est la même dans tous les fichiers, où qu'ils soient. - Importez les fichiers d'un même programme avec un chemin relatif,
import "ecran": le dossier peut être renommé ou copié sans changer le code. - Noms de fichiers et de dossiers : lettres, chiffres,
_,.et-, 32 caractères au plus ; un chemin tient en 128 caractères ; un support contient 1024 fichiers et dossiers.
Démarrer depuis startup
Au démarrage, un ordinateur exécute le fichier nommé startup à la racine de son support. Réduisez-le à une ligne qui importe le programme principal :
import "/apps/stock/main"import est le moyen pour un programme d'en lancer un autre : le programme principal s'exécute à l'intérieur, et comme il boucle en général pour toujours, startup ne se termine jamais. S'il se termine, l'invite revient.
Pour choisir le programme sans modifier startup, lisez son nom dans un fichier :
local nom = fs.read("/autorun") -- par exemple "stock", écrit avec : edit autorun
if nom ~= nil then
import("/apps/" .. nom:trim() .. "/main")
endUn exemple complet
L'écran des stocks de l'organisation ci-dessus, en quatre fichiers : deux bibliothèques, les réglages (le fichier config montré plus haut) et le programme. Il trouve un coffre à objet renforcé ou un coffre à côté de l'ordinateur (ou sur son Câble de données), et affiche une barre par objet, rouge quand le stock est bas.
local M = {}
function M.caler(texte, largeur)
texte = tostring(texte)
if #texte >= largeur then
return texte:sub(1, largeur)
end
return texte .. string.rep(" ", largeur - #texte)
end
function M.milliers(n)
if n < 0 then
return "-" .. M.milliers(-n)
end
local chiffres = tostring(math.floor(n))
local resultat = ""
while #chiffres > 3 do
resultat = " " .. chiffres:sub(-3) .. resultat
chiffres = chiffres:sub(1, -4)
end
return chiffres .. resultat
end
return Mlocal texte = import "texte" -- le même dossier : lib/texte
local M = {}
-- une barre horizontale : la partie pleine en couleur, le reste en gris
function M.barre(x, y, largeur, part, couleur)
local pleine = math.floor(largeur * math.max(0, math.min(1, part)) + 0.5)
term.set_cursor(x, y)
term.set_bg(couleur)
term.write(string.rep(" ", pleine))
term.set_bg(term.colors.gray)
term.write(string.rep(" ", largeur - pleine))
term.set_bg(term.colors.black)
end
-- une ligne du tableau : nom, quantité, barre
function M.ligne(y, nom, nombre, max)
term.set_cursor(1, y)
term.set_fg(term.colors.white)
term.write(texte.caler(nom, 14) .. texte.caler(texte.milliers(nombre), 9))
local couleur = term.colors.lime
if nombre < max * 0.2 then
couleur = term.colors.red
end
M.barre(24, y, 26, nombre / max, couleur)
end
return Mlocal config = import "/config"
local ui = import "/lib/ui"
local coffre = peripheral.find("inventory")
if coffre == nil then
error("pas d'inventaire à côté de l'ordinateur ni sur son câble")
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.titre)
local bas = false
for i, objet in ipairs(config.objets) do
local nombre = coffre.count(objet.id)
ui.ligne(2 + i, objet.nom, nombre, objet.max)
if nombre < objet.max * 0.2 then
bas = true
end
end
rs.set(config.cote_alarme, bas)
sleep(config.intervalle)
endimport "/apps/stock/main"Le programme principal dit quoi afficher ; lib/ui sait comment le dessiner ; config dit quels objets. Pour surveiller un second coffre renforcé sur un autre ordinateur, copiez les quatre fichiers et ne changez que config.
Partager du code entre ordinateurs
Les fichiers vivent sur le support, pas dans l'ordinateur. Trois façons d'amener une bibliothèque ou un programme sur un autre ordinateur :
- Emporter le support. Accroupi + clic droit sur l'ordinateur à main vide éjecte son support ; clic droit avec sur un autre ordinateur. Tous les fichiers suivent. Un ordinateur lit les supports de son âge et des âges précédents (Paquet de cartes perforées, Bobine de bande magnétique, Disquette, SSD), voir
Les ordinateurs. - Copier-coller. Ouvrez le fichier dans l'éditeur, Ctrl+A puis Ctrl+C ; ouvrez un fichier sur l'autre ordinateur et collez avec Ctrl+V. C'est le moyen le plus simple d'entrer dans un Microcontrôleur, dont la mémoire est soudée et qui ne prend aucun support. Ça marche aussi depuis ce site : le bouton Copier d'un bloc de code, puis Ctrl+V dans l'éditeur.
- L'envoyer par le réseau. À partir du Mini-ordinateur, des ordinateurs reliés par Câble de données (ou par radio, à partir du Micro-ordinateur) peuvent s'envoyer du texte. Deux tout petits programmes copient un fichier :
-- envoyer <fichier> <numéro d'ordinateur> : envoie un fichier à un autre ordinateur
local chemin, cible = arg[1], tonumber(arg[2])
local texte = assert(fs.read(chemin), "pas de fichier " .. tostring(chemin))
if net.send(cible, {chemin = chemin, texte = texte}, "fichiers") then
print("envoyé : " .. #texte .. " caractères")
else
print("ordinateur " .. tostring(cible) .. " injoignable")
end-- recevoir : écrit chaque fichier envoyé à cet ordinateur
print("en attente de fichiers... (ordinateur " .. os.id() .. ")")
while true do
local m = os.pull_event("message")
if m.channel == "fichiers" and type(m.data) == "table" then
fs.write(m.data.chemin, m.data.texte)
print("reçu " .. m.data.chemin .. " de " .. m.sender)
end
endLancez recevoir sur un ordinateur, puis envoyer lib/texte 12 sur l'autre (12 étant le numéro qu'affiche le receveur). Un message transporte jusqu'à 32768 caractères ; Réseaux explique le reste.
Voir aussi
import()etrequire(): la référence.Fonctions: fonctions locales et globales, et la règle sur les locales des fonctions englobantes.Un projet en plusieurs fichiers: un projet plus grand construit avec des modules.fs: les fichiers et les dossiers eux-mêmes.