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.
local objet = "minecraft:iron_ingot"
local nombre = 1234
print("Coffre renforcé : " .. nombre .. " x " .. objet)
print(string.format("%-10s %6d", "fer", nombre))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 :
print("Gare \"Nord\"")
print('Dites "bonjour" au bras mécanique')
print("C'est l'heure du train")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équence | donne |
|---|---|
\n | un retour à la ligne |
\t | une tabulation (l'écran passe à la prochaine colonne multiple de 4) |
\\ | une barre oblique inverse |
\" et \' | un guillemet |
\xNN | le caractère de code hexadécimal NN (\x41 donne A) |
\r, \0 | le 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'.
print("Nom\tStock")
print("Fer\t1250")
print("Ligne un\nLigne deux")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 :
local aide = [[
Commandes :
ouvrir ouvre la barrière
fermer ferme la barrière
etat affiche les horaires des trains]]
print(aide)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 :
local nom, stock = "Alliage d'andésite", 64
print(nom .. " : " .. stock .. " en stock")
print("Vitesse " .. 128 .. " tr/min")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.
local alimentee = true
print("Lampe allumée : " .. alimentee)snippet:2: attempt to concatenate a boolean value
local alimentee = true
print("Lampe allumée : " .. tostring(alimentee))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 :
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, " | "))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 :
local nom = " boitier de laiton "
print(string.upper(nom))
print(nom:trim():upper())
print(("zinc"):rep(3, "-"))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.
local mot = "Create"
print(#mot, mot:len())
print(mot:sub(1, 1), mot:sub(-1))
print(#"café")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.
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) .. "]")minecraft iron_ingot ingot iron []
Pour parcourir un texte caractère par caractère, prenez-les un à un avec sub(i, i) :
local code = "R2D"
for i = 1, #code do
print(i, code:sub(i, i))
end1 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.
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"))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 :
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")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.
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:"))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.
local ligne = "fer,or,,cuivre"
local morceaux = ligne:split(",")
print(#morceaux .. " morceaux")
for i, m in ipairs(morceaux) do
print(i, "[" .. m .. "]")
end4 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 :
local mots = ("ouvrir barrière nord"):split()
print(#mots .. " mots")
local lettres = ("FER"):split("")
print(lettres[1], lettres[2], lettres[3])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()(ouupper) met toutes les lettres dans la même casse, pour queOuvrir,OUVRIRetouvrirsoient égaux.
local tape = " Ouvrir Barrière "
local propre = tape:trim():lower()
print("[" .. propre .. "]")
print(propre == "ouvrir barrière")[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 :
local tape = "12"
print(tape * 2)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).
print(tonumber("42") + 1)
print(tonumber(" 3.5 "))
print(tonumber("12 objets"))
print(tonumber("ff", 16), tonumber("1010", 2))43 3.5 nil 255 10
Vérifiez toujours nil : le joueur peut taper n'importe quoi.
local tape = "douze"
local n = tonumber(tape)
if n == nil then
print("'" .. tape .. "' n'est pas un nombre")
else
print(n * 2)
end'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 :
print(10 / 2, 10 / 4, 1 / 3)
print(0.1 + 0.2, 2 ^ 60)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.
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))Vitesse : 128 tr/min Stress : 72.5 % Heure : 07:05 Coffre renforcé contient 1500 objets
| code | écrit | exemple | résultat |
|---|---|---|---|
%d | un nombre entier | format("%d", 42) | 42 |
%5d | aligné à droite sur 5 colonnes | format("%5d", 42) | 42 |
%-5d | aligné à gauche sur 5 colonnes | format("%-5d", 42) | 42 |
%05d | complété par des zéros | format("%05d", 42) | 00042 |
%+d | toujours avec un signe | format("%+d", 42) | +42 |
%.1f | un nombre avec 1 décimale (arrondie) | format("%.1f", 3.14159) | 3.1 |
%8.2f | 8 colonnes, 2 décimales | format("%8.2f", 3.14159) | 3.14 |
%s | n'importe quelle valeur en texte (comme tostring) | format("%s", true) | true |
%-10s | du texte complété à droite jusqu'à 10 colonnes | format("%-10s", "fer") | fer |
%.3s | les 3 premiers caractères | format("%.3s", "cuivre") | cui |
%x, %X | hexadécimal | format("%X", 255) | FF |
%% | un signe pour cent | format("%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 :
%dveut un nombre entier.string.format("%d", 2.5)s'arrête surbad argument #2 to 'format' (number has no integer representation): arrondissez d'abord avecmath.flooroumath.round, ou utilisez%.0f.- Chaque code
%demande une valeur. S'il en manque une, le programme s'arrête surbad 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 :
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))
endObjet 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).
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))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.
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"))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.
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> 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() :
while true do
write("> ")
executer(read())
endUn 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 :
-- 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)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 :
print("La ligne nord est fermée jusqu'à ce que les perceuses mécaniques finissent le tunnel.")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 :
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)
endLa 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 Lua | en Brass |
|---|---|
s:gsub("_", " ") | table.concat(s:split("_"), " ") |
s:match("^%s*(.-)%s*$") | s:trim() |
for m in s:gmatch("%S+") do | for _, 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.
local function remplacer(texte, ancien, nouveau)
return table.concat(texte:split(ancien), nouveau)
end
print(remplacer("lingot_de_fer", "_", " "))
print(remplacer("nord--sud--est", "--", " > "))lingot de fer nord > sud > est
Resserrer les espaces répétés en un seul :
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 ") .. "]")[beaucoup trop d'espaces]
Extraire le premier nombre d'un texte, tel qu'un capteur ou un autre mod pourrait l'écrire :
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"))128 72.5 nil
Compter les occurrences d'un texte : le nombre de morceaux après un split, moins un.
local trajet = "dépôt>mine>dépôt>ferme>dépôt"
print(#trajet:split("dépôt") - 1)3
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 renvoiesplitet qu'assembletable.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.