Create: Computing AgesDoc Brass
Le langage Brass

Travailler avec du texte

Assembler, découper, chercher, nettoyer et mettre en forme du texte : messages à l'écran, noms d'objets, commandes tapées par le joueur, fichiers de réglages.

Le texte est partout dans un programme : les messages à l'écran, les noms d'objets que renvoie un coffre à objet renforcé ("minecraft:iron_ingot"), la ligne qu'un joueur tape au clavier, un fichier de réglages sur la disquette, un message venu d'un autre ordinateur. En Brass, un morceau de texte est une chaîne (string), et la bibliothèque string sait la travailler.

Ce guide en est le côté pratique : comment faire les tâches de tous les jours. Chaque fonction utilisée ici est décrite en détail dans string.

Brass
local objet = "minecraft:iron_ingot"
local nombre = 1234
print("Coffre renforcé : " .. nombre .. " x " .. objet)
print(string.format("%-10s %6d", "fer", nombre))
Écran
Coffre renforcé : 1234 x minecraft:iron_ingot
fer          1234

Une chaîne ne change jamais. upper, sub, trim et les autres renvoient une nouvelle chaîne et laissent l'originale telle quelle : nom:upper() seul ne sert à rien, nom = nom:upper() garde le résultat.

Écrire du texte dans un programme

Une chaîne s'écrit entre guillemets doubles "..." ou simples '...'. Les deux se valent ; prenez celle qui vous évite d'échapper des caractères :

Brass
print("Gare \"Nord\"")
print('Dites "bonjour" au bras mécanique')
print("C'est l'heure du train")
Écran
Gare "Nord"
Dites "bonjour" au bras mécanique
C'est l'heure du train

Une barre oblique inverse (\) commence une séquence d'échappement, un caractère qu'on ne peut pas taper directement :

séquencedonne
\nun retour à la ligne
\tune tabulation (l'écran passe à la prochaine colonne multiple de 4)
\\une barre oblique inverse
\" et \'un guillemet
\xNNle caractère de code hexadécimal NN (\x41 donne A)
\r, \0le retour chariot, le caractère zéro

Toute autre lettre après une barre oblique inverse est une faute qui empêche le programme de compiler : invalid escape sequence '\q'.

Brass
print("Nom\tStock")
print("Fer\t1250")
print("Ligne un\nLigne deux")
Écran
Nom Stock
Fer 1250
Ligne un
Ligne deux

Du texte sur plusieurs lignes

Entre [[ et ]], une chaîne peut tenir sur plusieurs lignes et reste exactement telle qu'elle est écrite : aucune séquence d'échappement n'y est lue, et un retour à la ligne juste après [[ est ignoré. C'est idéal pour un écran d'aide ou un fichier de réglages par défaut :

Brass
local aide = [[
Commandes :
  ouvrir  ouvre la barrière
  fermer  ferme la barrière
  etat    affiche les horaires des trains]]
print(aide)
Écran
Commandes :
  ouvrir  ouvre la barrière
  fermer  ferme la barrière
  etat    affiche les horaires des trains

Pour travailler ligne par ligne sur un tel texte, coupez-le aux retours à la ligne avec texte:split("\n") (voir couper un texte en morceaux).

Assembler du texte

L'opérateur .. colle deux chaînes. Les nombres sont convertis en texte au passage, on peut donc les coller directement :

Brass
local nom, stock = "Alliage d'andésite", 64
print(nom .. " : " .. stock .. " en stock")
print("Vitesse " .. 128 .. " tr/min")
Écran
Alliage d'andésite : 64 en stock
Vitesse 128 tr/min

Les autres valeurs ne sont pas converties : nil, true, false et les tables arrêtent le programme. Passez-les d'abord par tostring.

Brass
local alimentee = true
print("Lampe allumée : " .. alimentee)
Écran
snippet:2: attempt to concatenate a boolean value
Brass
local alimentee = true
print("Lampe allumée : " .. tostring(alimentee))
Écran
Lampe allumée : true

Construire un texte dans une boucle

Chaque .. fabrique une nouvelle chaîne. Coller les morceaux un par un dans une longue boucle fonctionne, mais crée beaucoup de chaînes en chemin (elles prennent de la mémoire et du temps). Le meilleur outil : ranger les morceaux dans une table, puis les assembler en une fois avec table.concat, avec un séparateur si besoin :

Brass
local minerais = {"fer", "or", "cuivre", "zinc"}
print(table.concat(minerais, ", "))

local cases = {}
for niveau = 1, 5 do
  table.insert(cases, "N" .. niveau)
end
print(table.concat(cases, " | "))
Écran
fer, or, cuivre, zinc
N1 | N2 | N3 | N4 | N5

Appeler les fonctions de chaîne

Chaque fonction de la bibliothèque string s'appelle de deux façons : string.upper(nom), ou avec deux-points sur la chaîne elle-même, nom:upper(). La seconde forme est plus courte et se lit de gauche à droite, ce qui aide quand on enchaîne les appels :

Brass
local nom = "  boitier de laiton "
print(string.upper(nom))
print(nom:trim():upper())
print(("zinc"):rep(3, "-"))
Écran
  BOITIER DE LAITON
BOITIER DE LAITON
zinc-zinc-zinc

Une chaîne écrite directement dans le code demande des parenthèses avant les deux-points : ("zinc"):rep(3). Les nombres n'ont pas de méthodes : (42):upper() s'arrête sur attempt to index a number value. Convertissez d'abord, tostring(42).

Longueur et positions

#texte (ou texte:len()) est le nombre de caractères. Les positions commencent à 1, et une position négative compte depuis la fin : -1 est le dernier caractère, -2 l'avant-dernier.

Brass
local mot = "Create"
print(#mot, mot:len())
print(mot:sub(1, 1), mot:sub(-1))
print(#"café")
Écran
6   6
C   e
4

Brass compte des caractères, pas des octets : une lettre accentuée comme é compte pour un. (La taille d'un fichier, donnée par fs.size, est en octets, et é en prend deux.)

Découper : sub

texte:sub(i, j) donne les caractères de la position i à la position j, toutes deux comprises. Sans j, on va jusqu'au bout. Une position hors du texte n'est pas une erreur : on obtient simplement moins de texte, ou une chaîne vide.

Brass
local id = "minecraft:iron_ingot"
print(id:sub(1, 9))
print(id:sub(11))
print(id:sub(-5))
print(id:sub(11, 14))
print("[" .. id:sub(30, 40) .. "]")
Écran
minecraft
iron_ingot
ingot
iron
[]

Pour parcourir un texte caractère par caractère, prenez-les un à un avec sub(i, i) :

Brass
local code = "R2D"
for i = 1, #code do
  print(i, code:sub(i, i))
end
Écran
1   R
2   2
3   D

Chercher

find : où est-ce ?

texte:find(quoi) donne la position où quoi commence, ou nil s'il n'y est pas. La recherche est simple : chaque caractère est cherché tel quel (points, signes pour cent et crochets n'ont rien de spécial, contrairement aux motifs de Lua). Un troisième argument fait partir la recherche plus loin.

Brass
local id = "create:brass_ingot"
local deux_points = id:find(":")
print(deux_points)
print(id:sub(1, deux_points - 1), id:sub(deux_points + 1))
print(id:find("ingot"))
print(id:find("iron"))
Écran
7
create  brass_ingot
14
nil

find ne donne que le début. La fin de ce qui a été trouvé est debut + #quoi - 1.

Testez toujours nil avant d'utiliser le résultat : id:sub(1, id:find(":") - 1) s'arrête sur attempt to perform arithmetic on a nil value quand le texte n'a pas de deux-points.

Pour trouver toutes les occurrences, relancez chaque recherche juste après la précédente :

Brass
local journal = "fer,or,fer,charbon,fer"
local compte, depuis = 0, 1
while true do
  local pos = journal:find("fer", depuis)
  if pos == nil then
    break
  end
  compte = compte + 1
  depuis = pos + #"fer"
end
print(compte .. " fois")
Écran
3 fois

starts et ends

texte:starts(debut) et texte:ends(fin) répondent true ou false. C'est la façon la plus claire de trier des noms : fichiers de journal, objets d'un mod, commandes.

Brass
local fichiers = {"stock.log", "startup", "stock_old.log", "lib"}
for _, f in ipairs(fichiers) do
  if f:starts("stock") and f:ends(".log") then
    print(f)
  end
end
print(("create:cogwheel"):starts("create:"))
Écran
stock.log
stock_old.log
true

Couper un texte en morceaux

texte:split(separateur) coupe un texte à chaque séparateur et renvoie la liste (une table) des morceaux. C'est la clé pour lire les lignes d'un fichier, des valeurs séparées par des virgules, ou des mots.

Brass
local ligne = "fer,or,,cuivre"
local morceaux = ligne:split(",")
print(#morceaux .. " morceaux")
for i, m in ipairs(morceaux) do
  print(i, "[" .. m .. "]")
end
Écran
4 morceaux
1   [fer]
2   [or]
3   []
4   [cuivre]

Deux séparateurs à la suite donnent un morceau vide, comme le troisième ci-dessus. Sans séparateur, split coupe à chaque espace, donc deux espaces donnent aussi un mot vide ; avec "", il coupe en caractères :

Brass
local mots = ("ouvrir  barrière nord"):split()
print(#mots .. " mots")
local lettres = ("FER"):split("")
print(lettres[1], lettres[2], lettres[3])
Écran
4 mots
F   E   R

Quand vous voulez de vrais mots, sautez les morceaux vides (voir lire une commande plus bas).

Nettoyer ce que tape le joueur

Un texte tapé au clavier (avec read) ou collé est rarement propre : des espaces aux bouts, des majuscules là où on ne les attendait pas. Deux appels règlent presque tout :

  • texte:trim() enlève les espaces, tabulations et retours à la ligne aux deux bouts ;
  • texte:lower() (ou upper) met toutes les lettres dans la même casse, pour que Ouvrir, OUVRIR et ouvrir soient égaux.
Brass
local tape = "   Ouvrir Barrière  "
local propre = tape:trim():lower()
print("[" .. propre .. "]")
print(propre == "ouvrir barrière")
Écran
[ouvrir barrière]
true

Nombres et texte

read et fs.read donnent toujours du texte, même quand il ressemble à un nombre. Brass ne convertit pas de lui-même un texte en nombre dans un calcul :

Brass
local tape = "12"
print(tape * 2)
Écran
snippet:2: attempt to perform arithmetic on a strin
g value

tonumber(texte) fait la conversion. Elle accepte des espaces autour du nombre, les décimales (avec un point), les exposants (1e3) et l'hexadécimal (0xFF), et renvoie nil quand le texte n'est pas un nombre. Donnez une base en second argument pour les autres systèmes (de 2 à 36).

Brass
print(tonumber("42") + 1)
print(tonumber(" 3.5 "))
print(tonumber("12 objets"))
print(tonumber("ff", 16), tonumber("1010", 2))
Écran
43
3.5
nil
255 10

Vérifiez toujours nil : le joueur peut taper n'importe quoi.

Brass
local tape = "douze"
local n = tonumber(tape)
if n == nil then
  print("'" .. tape .. "' n'est pas un nombre")
else
  print(n * 2)
end
Écran
'douze' n'est pas un nombre

Dans l'autre sens, tostring(valeur) change n'importe quoi en texte, et .. le fait pour les nombres. Les nombres entiers s'affichent sans virgule ; les autres avec au plus 14 chiffres significatifs, et les très grands ou très petits avec un exposant :

Brass
print(10 / 2, 10 / 4, 1 / 3)
print(0.1 + 0.2, 2 ^ 60)
Écran
5   2.5 0.33333333333333
0.3 1.1529215046068e+18

Mettre en forme avec string.format

string.format(modele, valeurs...) construit un texte à partir d'un modèle : chaque code % du modèle est remplacé par la valeur suivante, écrite comme le code l'indique. C'est l'outil des décimales fixes, des colonnes alignées et des zéros devant.

Brass
print(string.format("Vitesse : %d tr/min", 128))
print(string.format("Stress : %.1f %%", 72.456))
print(string.format("Heure : %02d:%02d", 7, 5))
print(("%s contient %d objets"):format("Coffre renforcé", 1500))
Écran
Vitesse : 128 tr/min
Stress : 72.5 %
Heure : 07:05
Coffre renforcé contient 1500 objets
codeécritexemplerésultat
%dun nombre entierformat("%d", 42)42
%5daligné à droite sur 5 colonnesformat("%5d", 42)42
%-5daligné à gauche sur 5 colonnesformat("%-5d", 42)42
%05dcomplété par des zérosformat("%05d", 42)00042
%+dtoujours avec un signeformat("%+d", 42)+42
%.1fun nombre avec 1 décimale (arrondie)format("%.1f", 3.14159)3.1
%8.2f8 colonnes, 2 décimalesformat("%8.2f", 3.14159)3.14
%sn'importe quelle valeur en texte (comme tostring)format("%s", true)true
%-10sdu texte complété à droite jusqu'à 10 colonnesformat("%-10s", "fer")fer
%.3sles 3 premiers caractèresformat("%.3s", "cuivre")cui
%x, %Xhexadécimalformat("%X", 255)FF
%%un signe pour centformat("%d%%", 50)50%

%i est identique à %d. Largeurs et décimales vont jusqu'à 99. Il n'y a ni %q, ni %e, ni %g, ni %c, ni %o : ils arrêtent le programme avec invalid conversion '%q' to 'format'.

Deux fautes à connaître :

  • %d veut un nombre entier. string.format("%d", 2.5) s'arrête sur bad argument #2 to 'format' (number has no integer representation) : arrondissez d'abord avec math.floor ou math.round, ou utilisez %.0f.
  • Chaque code % demande une valeur. S'il en manque une, le programme s'arrête sur bad argument #2 to 'format' (no value).

Colonnes alignées

Les largeurs alignent un tableau à l'écran, quelle que soit la longueur de chaque nom ou nombre :

Brass
local stock = {
  {nom = "Lingot de fer", nombre = 1250, max = 2000},
  {nom = "Lingot d'or", nombre = 87, max = 500},
  {nom = "Lingot de laiton", nombre = 640, max = 640},
}
print(string.format("%-16s %6s %6s", "Objet", "Nombre", "Plein"))
for _, s in ipairs(stock) do
  print(string.format("%-16s %6d %5.1f%%", s.nom, s.nombre, s.nombre / s.max * 100))
end
Écran
Objet            Nombre  Plein
Lingot de fer      1250  62.5%
Lingot d'or          87  17.4%
Lingot de laiton    640 100.0%

Centrer et tracer des lignes

string.rep(texte, n) répète un texte : une ligne de = sur toute la largeur, ou les espaces qui centrent un titre. term.get_size() donne la largeur de l'écran (51 colonnes sur un Micro-ordinateur, 40 sur le Calculateur à tubes et le Microcontrôleur, 64 sur l'Ordinateur moderne).

Brass
local function centrer(texte, largeur)
  local gauche = math.floor((largeur - #texte) / 2)
  return string.rep(" ", gauche) .. texte
end

local largeur = term.get_size().w
print(centrer("GARE CENTRALE", largeur))
print(string.rep("=", largeur))
Écran
                   GARE CENTRALE
===================================================

Les caractères et leurs codes

Chaque caractère a un numéro, son code : "A" vaut 65, "a" 97, "0" 48. texte:byte(i) donne le code du caractère à la position i (le premier par défaut), et string.char(...) fabrique un texte à partir de codes. Lettres et chiffres se suivent : "0" à "9" vont de 48 à 57, "A" à "Z" de 65 à 90, "a" à "z" de 97 à 122.

Brass
print(("A"):byte(), ("Create"):byte(2))
print(string.char(72, 105, 33))

local function est_chiffre(c)
  local code = c:byte()
  return code ~= nil and code >= 48 and code <= 57
end
print(est_chiffre("7"), est_chiffre("x"))

local function majuscule(mot)
  return mot:sub(1, 1):upper() .. mot:sub(2)
end
print(majuscule("andésite"))
Écran
65  114
Hi!
true    false
Andésite

Les chaînes se comparent aussi avec < et >, code de caractère par code de caractère : "abricot" < "banane" vaut true. C'est ainsi que table.sort range une liste de noms. Les majuscules passent avant les minuscules ("Zinc" < "fer"), et les lettres accentuées après toutes les autres ("étain" > "zinc") : comparez des versions lower() pour un ordre plus naturel.

Recettes

Lire une commande tapée par le joueur

Un programme de contrôle attend souvent une commande : ouvrir nord, vitesse 96. Les étapes sont toujours les mêmes : nettoyer la ligne, la couper en mots (en sautant les mots vides que laissent les espaces en trop), puis regarder le premier mot.

Brass
local function mots_de(ligne)
  local mots = {}
  for _, m in ipairs(ligne:trim():split(" ")) do
    if m ~= "" then
      table.insert(mots, m)
    end
  end
  return mots
end

local function executer(ligne)
  local mots = mots_de(ligne)
  local commande = (mots[1] or ""):lower()
  if commande == "ouvrir" then
    print("ouverture : " .. (mots[2] or "toutes les barrières"))
  elseif commande == "vitesse" then
    local tpm = tonumber(mots[2])
    if tpm == nil then
      print("usage : vitesse <tr/min>")
    else
      print("vitesse réglée à " .. tpm .. " tr/min")
    end
  elseif commande ~= "" then
    print("commande inconnue : " .. commande)
  end
end

-- quelques lignes que le joueur pourrait taper
for _, ligne in ipairs({"ouvrir nord", "  VITESSE   96 ", "vitesse vite", "danser"}) do
  print("> " .. ligne)
  executer(ligne)
end
Écran
> ouvrir nord
ouverture : nord
>   VITESSE   96
vitesse réglée à 96 tr/min
> vitesse vite
usage : vitesse <tr/min>
> danser
commande inconnue : danser

Dans le vrai programme, les lignes viennent du clavier, avec read() :

Brass
while true do
  write("> ")
  executer(read())
end

Un fichier de réglages

Laissez les joueurs modifier un programme sans toucher à son code : mettez les réglages dans un fichier texte, un cle = valeur par ligne, et lisez-le au démarrage. Ce lecteur saute les lignes vides et celles qui commencent par #, change les nombres en nombres et true/false en booléens :

Brass
-- le fichier, tel qu'un joueur l'écrirait avec "edit reglages"
fs.write("reglages", [[
# Contrôle de la ferme à fer
gare = Mine de fer
seuil = 1500
cote_alarme = top
debug = false
]])

local function charger_reglages(chemin)
  local reglages = {}
  local texte = fs.read(chemin)
  if texte == nil then
    return reglages  -- pas de fichier : une table vide, les valeurs par défaut s'appliquent
  end
  for n, ligne in ipairs(texte:split("\n")) do
    ligne = ligne:trim()
    if ligne ~= "" and not ligne:starts("#") then
      local egal = ligne:find("=")
      if egal == nil then
        error(chemin .. " ligne " .. n .. " : '=' attendu")
      end
      local cle = ligne:sub(1, egal - 1):trim()
      local valeur = ligne:sub(egal + 1):trim()
      if tonumber(valeur) ~= nil then
        valeur = tonumber(valeur)
      elseif valeur == "true" or valeur == "false" then
        valeur = valeur == "true"
      end
      reglages[cle] = valeur
    end
  end
  return reglages
end

local r = charger_reglages("reglages")
print(r.gare, r.seuil + 1, r.cote_alarme, r.debug)
Écran
Mine de fer 1501    top false

Les valeurs par défaut se donnent avec or : local cote = r.cote_alarme or "back". (Attention aux booléens : r.debug or true vaut toujours true.) Une autre façon de ranger des réglages est un fichier Brass qui renvoie une table, chargé avec import : voir Programmes en plusieurs fichiers.

Couper un long texte à la largeur de l'écran

print passe à la ligne quand la ligne est pleine, même au milieu d'un mot :

Brass
print("La ligne nord est fermée jusqu'à ce que les perceuses mécaniques finissent le tunnel.")
Écran
La ligne nord est fermée jusqu'à ce que les perceus
es mécaniques finissent le tunnel.

Pour couper entre les mots, construisez les lignes vous-même. Cette fonction coupe aussi un mot plus long qu'une ligne entière :

Brass
local function couper(texte, largeur)
  local lignes = {}
  local ligne = ""
  for _, mot in ipairs(texte:split(" ")) do
    while #mot > largeur do
      if ligne ~= "" then
        table.insert(lignes, ligne)
        ligne = ""
      end
      table.insert(lignes, mot:sub(1, largeur))
      mot = mot:sub(largeur + 1)
    end
    if mot == "" then
      -- il ne reste rien de ce mot
    elseif ligne == "" then
      ligne = mot
    elseif #ligne + 1 + #mot <= largeur then
      ligne = ligne .. " " .. mot
    else
      table.insert(lignes, ligne)
      ligne = mot
    end
  end
  if ligne ~= "" then
    table.insert(lignes, ligne)
  end
  return lignes
end

local info = "La ligne nord est fermée jusqu'à ce que les perceuses mécaniques finissent le tunnel. Les trains pour la mine de fer partent du quai 2."
for _, l in ipairs(couper(info, term.get_size().w)) do
  print(l)
end
Écran
La ligne nord est fermée jusqu'à ce que les
perceuses mécaniques finissent le tunnel. Les
trains pour la mine de fer partent du quai 2.

La fonction renvoie une liste de lignes plutôt que de les afficher : vous pouvez aussi les compter (pour centrer un texte en hauteur) ou les dessiner à l'endroit voulu avec term.set_cursor et term.write.

Se passer des motifs

Lua a des motifs (patterns), un petit langage de recherche (%d+, %s*, (.-)) utilisé par string.match, string.gmatch et string.gsub. Brass n'en a aucun : find cherche le texte exactement tel qu'il est écrit, et match, gmatch et gsub n'existent pas. Les tâches courantes s'écrivent en peu de lignes avec split, find, sub et trim :

en Luaen Brass
s:gsub("_", " ")table.concat(s:split("_"), " ")
s:match("^%s*(.-)%s*$")s:trim()
for m in s:gmatch("%S+") dofor _, m in ipairs(s:split(" ")) do en sautant m == ""
s:match("^(%w+)=(.*)$")local egal = s:find("="), puis s:sub(1, egal - 1) et s:sub(egal + 1)
s:find("^prefixe")s:starts("prefixe")
s:find("%.log$")s:ends(".log")
tonumber(s:match("%d+"))une boucle sur les caractères (ci-dessous)

Remplacer un texte par un autre : couper au texte à remplacer, recoller avec le nouveau.

Brass
local function remplacer(texte, ancien, nouveau)
  return table.concat(texte:split(ancien), nouveau)
end
print(remplacer("lingot_de_fer", "_", " "))
print(remplacer("nord--sud--est", "--", " > "))
Écran
lingot de fer
nord > sud > est

Resserrer les espaces répétés en un seul :

Brass
local function resserrer(texte)
  local mots = {}
  for _, m in ipairs(texte:split(" ")) do
    if m ~= "" then
      table.insert(mots, m)
    end
  end
  return table.concat(mots, " ")
end
print("[" .. resserrer("  beaucoup   trop    d'espaces ") .. "]")
Écran
[beaucoup trop d'espaces]

Extraire le premier nombre d'un texte, tel qu'un capteur ou un autre mod pourrait l'écrire :

Brass
local function premier_nombre(texte)
  local chiffres = ""
  for i = 1, #texte do
    local c = texte:sub(i, i)
    if (c >= "0" and c <= "9") or (c == "." and chiffres ~= "") then
      chiffres = chiffres .. c
    elseif chiffres ~= "" then
      break
    end
  end
  return tonumber(chiffres)
end
print(premier_nombre("Vitesse : 128 tr/min"))
print(premier_nombre("Stress 72.5% utilisé"))
print(premier_nombre("aucun chiffre ici"))
Écran
128
72.5
nil

Compter les occurrences d'un texte : le nombre de morceaux après un split, moins un.

Brass
local trajet = "dépôt>mine>dépôt>ferme>dépôt"
print(#trajet:split("dépôt") - 1)
Écran
3
Note

Les chaînes coûtent de la mémoire : environ une cellule pour 8 caractères (voir Vitesse, mémoire et limites), et une chaîne contient au plus 65536 caractères (string too long). find, split, print et fs.write coûtent aussi quelques instructions de plus sur les très longs textes. Pour les messages et les fichiers de réglages de tous les jours, rien de tout cela ne compte.

Voir aussi

  • string : toutes les fonctions de chaîne, avec leurs arguments et leurs cas limites.
  • Tables : les listes que renvoie split et qu'assemble table.concat.
  • Erreurs et débogage : ce que veulent dire les messages d'erreur ci-dessus, et comment les rattraper.
  • fs : lire et écrire des fichiers texte.