Retour au cours

games / lua

Manipulation de chaînes et patterns Lua

Leçon 111 exercice

Explication

Ce que vous allez apprendre

  • Utiliser les fonctions de base de la bibliothèque string (sub, upper, format...)
  • Écrire un pattern Lua avec les classes de caractères (%d, %a, %s, %w)
  • Extraire une information précise avec string.match et ses groupes de capture
  • Itérer sur toutes les correspondances avec string.gmatch
  • Rechercher et remplacer avec string.gsub, y compris avec une fonction de remplacement dynamique

Dans quel contexte ?

Un script serveur reçoit un message de chat brut du joueur et doit en extraire une commande et ses arguments (par exemple /tp 100 50 20 doit donner la commande tp et trois nombres), tout en validant que le format est correct avant de l'exécuter. C'est exactement le genre de tâche pour laquelle les patterns Lua, plus légers qu'une regex complète, sont conçus.

Pourquoi Lua n'utilise pas les regex classiques

Si vous avez déjà utilisé les expressions régulières (regex) dans un autre langage, sachez que Lua fait un choix différent : un système de "patterns" plus simple, plus rapide, mais moins puissant. Ce choix est délibéré — Lua vise la légèreté (le langage entier tient dans un exécutable minuscule), et un moteur de regex complet aurait ajouté une complexité disproportionnée par rapport aux besoins réels de la plupart des scripts.

L'intuition : des classes de caractères, pas une syntaxe complète

Un pattern Lua se lit comme une petite recette : %d signifie "un chiffre", %a "une lettre", %s "un espace". Ces classes se combinent avec des quantificateurs (+ pour "un ou plusieurs", * pour "zéro ou plus") pour décrire une forme générale à rechercher, plutôt qu'un texte exact. C'est un vocabulaire plus restreint que les regex complètes, mais qui couvre l'essentiel des besoins courants (extraire un nombre, valider un format simple, découper une chaîne).

Les trois outils, chacun pour un usage précis

string.match extrait la première correspondance trouvée (et ses groupes capturés entre parenthèses) — utile pour "extraire une information précise d'un texte". string.gmatch renvoie un itérateur qui parcourt toutes les correspondances successives — utile pour "traiter chaque élément d'une liste encodée en texte". string.gsub recherche et remplace, en renvoyant en prime le nombre de remplacements effectués — utile pour "transformer un texte selon une règle".

ClasseSignifieExemple de correspondance
%dun chiffre"5" dans "score:5"
%aune lettre"user" dans "user_42"
%sun espacel'espace dans "a b"
%walphanumérique"a1" dans "a1!"

Piège fréquent

Les caractères . % + - * ? [ ] ^ $ ont un sens spécial dans un pattern Lua. Chercher un point littéral (comme dans un prix "9.99") sans l'échapper avec %. fera correspondre N'IMPORTE QUEL caractère à cette position, pas seulement un point — un bug silencieux facile à manquer en test.

Le piège à connaître absolument

Certains caractères ont un sens spécial dans un pattern (., %, +, -, *, ?, les crochets, ^, $). Si vous cherchez ces caractères littéralement (par exemple un point dans un prix "9.99"), il faut les échapper avec %, sinon le pattern les interprète comme une instruction plutôt que comme du texte à chercher — une source d'erreurs fréquente en particulier avec des données saisies par un utilisateur, où le point ou le signe dollar peuvent apparaître sans prévenir.

Commandes & code

Manipulation de chaînes et patterns Lua

Lua n'utilise PAS les expressions régulières classiques (regex) mais un système de "patterns" plus simple et plus rapide, propre au langage.

lua
-- Fonctions de base de la bibliothèque "string"
local s = "Technologik e-learning"

print(string.len(s), #s)              -- longueur : 23  23  (# équivaut à string.len sur une chaîne)
print(string.upper(s))                 -- "TECHNOLOGIK E-LEARNING"
print(string.lower(s))                  -- "technologik e-learning"
print(string.sub(s, 1, 11))              -- "Technologik" (indices 1-based, inclusifs)
print(string.sub(s, -9))                  -- "e-learning" (index négatif = depuis la fin)
print(string.rep("ab", 3))                 -- "ababab"
print(string.reverse("Lua"))                -- "auL"
lua
-- string.format : formatage similaire à printf en C
print(string.format("Joueur: %s | Niveau: %d | Précision: %.2f%%", "Alice", 12, 87.456))
-- "Joueur: Alice | Niveau: 12 | Précision: 87.46%"

print(string.format("%05d", 42))     -- "00042" (padding avec des zéros)
print(string.format("%x", 255))       -- "ff" (hexadécimal)
lua
-- Classes de caractères des patterns Lua (différent de regex mais logique similaire)
-- %a lettres    %d chiffres    %s espaces    %w alphanumériques    %p ponctuation
-- %u majuscules %l minuscules  %c contrôle    %x hexadécimal
-- Version majuscule = complément : %A = tout sauf lettre, etc.
-- Quantificateurs : * (0 ou plus), + (1 ou plus), - (0 ou plus, non-greedy), ? (0 ou 1)

print(string.match("Score: 87", "%d+"))         -- "87" (une ou plusieurs chiffres)
print(string.match("user_42", "%a+"))            -- "user" (lettres uniquement, s'arrête au _)
print(string.match("  texte  ", "^%s*(.-)%s*$"))  -- "texte" (trim des espaces via un groupe capturé)
lua
-- string.match avec groupes de capture (parenthèses)
local email = "alice@technologik.local"
local user, domain = string.match(email, "(%w+)@([%w%.]+)")
print(user, domain)   -- "alice"  "technologik.local"

-- Validation basique d'email (pattern simplifié, pas exhaustif comme une vraie regex email RFC)
local function isValidEmailFormat(s)
  return string.match(s, "^[%w%.]+@[%w%.]+%.%a+$") ~= nil
end
print(isValidEmailFormat("test@example.com"))   -- true
lua
-- string.gmatch : itérateur qui retourne toutes les correspondances successives (comme findall en regex)
local csv = "epee,arc,baton,bouclier"
for item in string.gmatch(csv, "[^,]+") do   -- "[^,]+" : tout sauf une virgule
  print(item)
end
-- epee / arc / baton / bouclier, un par ligne

-- Extraction de toutes les paires clé=valeur d'une chaîne de configuration
local config = "width=800;height=600;fullscreen=false"
for key, value in string.gmatch(config, "(%w+)=(%w+)") do
  print(key .. " -> " .. value)
end
lua
-- string.gsub : recherche ET remplacement, retourne aussi le nombre de remplacements effectués
local text = "Le prix est 100 EUR, remise de 10 EUR"
local result, count = string.gsub(text, "%d+ EUR", "[MASQUÉ]")
print(result)   -- "Le prix est [MASQUÉ], remise de [MASQUÉ]"
print(count)     -- 2

-- gsub avec une fonction de remplacement dynamique
local template = "Bonjour {name}, tu as {age} ans"
local data = { name = "Alice", age = "17" }
local personalized = string.gsub(template, "{(%w+)}", function(key)
  return data[key] or "{" .. key .. "}"   -- conserve le placeholder si la clé est inconnue
end)
print(personalized)   -- "Bonjour Alice, tu as 17 ans"
lua
-- Piège classique : les caractères magiques doivent être échappés avec "%" dans un pattern
-- ( ) . % + - * ? [ ] ^ $ sont tous des caractères spéciaux en pattern Lua
local function escapePattern(s)
  return (s:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1"))   -- préfixe chaque caractère magique par %
end

local userInput = "prix: 9.99$"
local escaped = escapePattern(userInput)
print(string.find("le prix: 9.99$ est fixe", escaped))   -- recherche littérale, pas interprétée comme pattern

Résumé

  • Les patterns Lua ne sont PAS des regex classiques : classes %a %d %s %w, quantificateurs * + - ?.
  • string.match extrait, string.gmatch itère toutes les correspondances, string.gsub remplace (et compte).
  • Les caractères magiques (. % + - * ? [ ] ^ $) doivent être échappés avec % pour une recherche littérale.
  • string.format couvre le formatage type printf, indispensable pour construire des messages structurés.

Exercices pratiques

1 disponible
1

Mission : la commande /tp qui accepte n'importe quoi

Objectif : Corriger un parseur de commande de chat qui valide mal ses arguments à cause de patterns Lua mal échappés ou trop permissifs.

Contexte

Un script serveur parse les commandes de chat des joueurs pour extraire une commande et ses arguments :

lua
local function parseCommand(message)
  local cmd, args = string.match(message, "^/(%a+)%s+(.+)$")
  return cmd, args
end

local cmd, args = parseCommand("/tp 100 50 20")
print(cmd, args)   -- "tp"  "100 50 20"

-- Le script doit aussi valider un prix affiché au joueur, format "9.99"
local function isValidPrice(s)
  return string.match(s, "^%d+.%d%d$") ~= nil
end
print(isValidPrice("9.99"))    -- true, attendu
print(isValidPrice("9x99"))    -- true aussi ! ne devrait pourtant PAS être un prix valide

L'équipe QA signale que isValidPrice accepte des chaînes qui ne sont clairement pas des prix, ce qui laisse passer des données invalides dans le système de boutique.

Résoudre l’exercice →