wandres.dev
PATRONES · no son expresiones regulares

Clases de caracteres y cuantificadores

Las diez clases predefinidas y su versión complementada, los conjuntos entre corchetes con sus rangos y sus trampas, los tres cuantificadores codiciosos y el único perezoso que existe, y el escape de los caracteres mágicos con el porcentaje.

⏱ 19 min

El vocabulario entero del lenguaje de patrones cabe en dos tablas y cuatro símbolos, y aun así casi todos los errores que verás en producción nacen aquí: una clase que no incluye lo que creías, un conjunto cuyo guion se interpretó como rango, un cuantificador codicioso que se tragó media línea, un carácter mágico sin escapar que convirtió un dato del usuario en una instrucción. Este capítulo agota el vocabulario. No hay nada más que memorizar en todo el nivel: lo que viene después son composiciones de estas piezas y las funciones que las consumen.

🎯 Al terminar esta lección sabrás
  • Recitar las clases predefinidas y construir su complemento sin dudar.
  • Escribir conjuntos entre corchetes controlando rangos, negación y caracteres que se vuelven literales.
  • Distinguir la coincidencia codiciosa de la perezosa y predecir qué devuelve cada una.
  • Escapar datos externos antes de meterlos en un patrón, y saber por qué es obligatorio.

Clases predefinidas y su complemento

Una clase predefinida es un porcentaje seguido de una letra, y designa un conjunto de bytes. Son diez, y la regla que las ordena es de una economía notable: la letra en mayúscula denota el complemento de la letra en minúscula. No hay que aprender veinte símbolos, sino diez y una regla.

Clase Designa Complemento
%a letras %A
%d dígitos decimales %D
%l letras minúsculas %L
%u letras mayúsculas %U
%s espacios en blanco %S
%w letras y dígitos %W
%p signos de puntuación %P
%c caracteres de control %C
%x dígitos hexadecimales %X
%g imprimibles salvo el espacio %G

Fuera de esa tabla queda el punto, que designa cualquier byte, incluido el salto de línea: no hay en Lua distinción entre modo normal y modo de línea única, porque el punto siempre lo abarca todo. Y hay dos advertencias que conviene grabar antes de escribir el primer patrón serio. La primera: %w no incluye el guion bajo, a diferencia de la clase equivalente de casi todos los motores de expresiones regulares; si buscas identificadores tienes que escribir el conjunto a mano. La segunda: estas clases se resuelven consultando las funciones de clasificación del C estándar, de modo que dependen de la configuración regional del proceso y, en la configuración por defecto, solo clasifican correctamente el rango ASCII. Una vocal acentuada codificada en UTF-8 son dos bytes, y ninguno de los dos es una letra a ojos de %a.

print(("Ada Lovelace 1815"):match("%a+"))        --> Ada
print(("Ada Lovelace 1815"):match("%d+"))        --> 1815
print(("cadena_con_guion"):match("%w+"))         --> cadena
print(("cadena_con_guion"):match("[%w_]+"))      --> cadena_con_guion
print(("año"):match("%a+"))                      --> a   (la enne no es letra)

Conjuntos entre corchetes

Cuando ninguna clase predefinida sirve, se construye un conjunto explícito entre corchetes. Dentro caben caracteres sueltos, rangos escritos con un guion entre extremos, y clases predefinidas, que pueden mezclarse libremente. Si el primer carácter del conjunto es el acento circunflejo, el conjunto se complementa.

local id = "temp_2 = valor_3"
print(id:match("[%a_][%w_]*"))       --> temp_2   (identificador clasico)
print(id:match("[^%s=]+"))           --> temp_2   (todo menos espacio o igual)
print(("nota: 7,5"):match("[0-9,]+")) --> 7,5
print(("a-b"):match("[%a%-]+"))      --> a-b      (guion escapado, literal)

Tres detalles del interior de los corchetes se cobran caro cuando se ignoran. El guion es literal si aparece al principio o al final del conjunto, y designa un rango en cualquier otra posición; si lo quieres literal en medio, escápalo. El corchete de cierre no tiene el privilegio posicional que le concede POSIX: en Lua hay que escaparlo siempre para incluirlo. Y el acento circunflejo solo niega en la primera posición; en cualquier otra es un carácter corriente.

🔍

El complemento incluye todo

Un conjunto negado abarca los doscientos cincuenta y seis valores de byte menos los excluidos, saltos de línea y bytes altos de UTF-8 incluidos. Casi nunca es lo que quieres si el texto es humano.

🧩

Rangos por código, no por alfabeto

Un rango se resuelve comparando valores de byte. El que va de la a a la z no contiene ninguna letra acentuada, y el que va de la A a la z contiene además seis signos de puntuación.

⚠️

Sin insensibilidad a la caja

No existe una bandera que ignore mayúsculas. O escribes el conjunto con ambas cajas o normalizas la cadena antes de buscar.

🧵

Sin ceros incrustados en el patrón

La cadena buscada puede contener bytes nulos, pero el patrón se recorre hasta su terminador y por tanto no puede llevarlos en literal cómodamente.

Codiciosos y perezoso

Un cuantificador afecta siempre y solo a la clase inmediatamente anterior, nunca a un grupo. Hay cuatro, y de ellos tres son codiciosos.

Símbolo Repeticiones Comportamiento
* cero o más codicioso, cede al retroceder
+ una o más codicioso, cede al retroceder
? cero o una codicioso
- cero o más perezoso, crece al retroceder

Codicioso significa que el motor consume todo lo que puede y después retrocede cediendo caracteres hasta que el resto del patrón encaja. Perezoso significa lo contrario: empieza sin consumir nada y va cediendo terreno hacia delante mientras el resto del patrón siga sin encajar. Con el mismo texto y patrones que solo difieren en ese símbolo, los resultados divergen radicalmente.

local s = 'dijo "hola" y luego "adios"'
print(s:match('".*"'))     --> "hola" y luego "adios"
print(s:match('".-"'))     --> "hola"

local html = "campo: 12345 unidades"
print(html:match("%d*"))   --> cadena vacia: encaja al inicio con cero digitos
print(html:match("%d+"))   --> 12345
flowchart LR
A[Motor en una posicion] --> B[Cuantificador codicioso]
B --> C[Consume el maximo posible]
C --> D[Intenta encajar el resto del patron]
D --> E[Si falla cede un caracter y reintenta]
E --> D
A --> F[Cuantificador perezoso]
F --> G[Consume cero caracteres]
G --> H[Intenta encajar el resto del patron]
H --> I[Si falla consume uno mas y reintenta]
I --> H

Nótese la asimetría del repertorio: hay tres cuantificadores codiciosos y uno solo perezoso, que equivale a la estrella. No existe «una o más, perezoso» ni «cero o una, perezoso». Cuando necesitas la primera, se escribe repitiendo la clase y aplicando el perezoso a la copia, de modo que la primera repetición sea obligatoria y las demás opcionales.

print(("aaa!"):match("%a%a-"))     --> a    (una obligatoria, el resto perezoso)
print(("aaa!"):match("%a%a*"))     --> aaa  (una obligatoria, el resto codicioso)

Los mágicos y su escape

Once caracteres tienen significado en un patrón: el circunflejo, el dólar, el asterisco, el más, la interrogación, el punto, el paréntesis de apertura y el de cierre, el corchete de apertura, el porcentaje y el guion. Para usarlos como texto literal se les antepone un porcentaje. La regla exacta del manual es más amplia y más útil de lo que parece: un porcentaje seguido de cualquier carácter no alfanumérico representa ese carácter. Es decir, escapar de más no rompe nada mientras el carácter escapado no sea una letra ni un dígito, lo que permite escribir una función de escapado en una línea.

local function literal(s)
  return (s:gsub("%W", "%%%0"))       -- escapa todo lo no alfanumerico
end

local buscado = literal("total (neto): 3.5%")
print(("el total (neto): 3.5% de ayer"):find(buscado))   --> encuentra la posicion
print(("versión 1.2.3"):match("1%.2%.3"))                --> 1.2.3
Un solo símbolo con tres oficios: la elegancia del porcentaje

La elección del porcentaje como carácter de escape parece un capricho tipográfico y es una de las decisiones más finas del diseño. La razón inmediata es que la barra invertida ya está tomada: es el escape de los literales de cadena del propio lenguaje, así que un patrón que buscara un dígito habría tenido que escribirse con la barra duplicada, y uno que buscara una barra literal, con cuatro barras seguidas. Ese fenómeno, la plaga de las barras invertidas, ha costado incontables horas en los lenguajes que sí eligieron ese camino, y Lua lo esquiva por completo: lo que escribes en el literal de cadena es exactamente lo que ve el motor. Pero lo interesante viene después. El porcentaje no tiene un oficio, tiene tres, y el motor los distingue mirando únicamente el carácter que le sigue. Si lo que sigue es una letra, el porcentaje introduce una clase de caracteres. Si es un dígito, introduce una referencia a una captura, ya sea hacia atrás dentro del patrón o hacia el reemplazo en una sustitución. Si es cualquier otra cosa, escapa ese carácter y lo vuelve literal. Tres funciones semánticamente muy distintas, cero ambigüedad, un solo símbolo, y un analizador que decide con una sola comprobación de tipo de carácter y sin estado. Compárese con la maraña de prefijos, banderas y grupos con nombre que administran esas mismas tres funciones en un motor de expresiones regulares moderno, y se entiende el precio real de ese código diez veces mayor. La lección de diseño trasciende los patrones: cuando el espacio es escaso, no se recortan capacidades, se fusionan notaciones buscando una regla de desambiguación que quepa en una frase.

Hay una última regla que cierra el vocabulario y que en realidad pertenece a la categoría de los anclajes: el circunflejo al principio del patrón ancla la coincidencia al inicio del sujeto, y el dólar al final del patrón la ancla al final. En cualquier otra posición son caracteres corrientes que se buscan a sí mismos, lo que significa que no hace falta escaparlos si están en medio, aunque escaparlos tampoco molesta. Los dos anclajes juntos convierten una búsqueda en una comprobación de forma completa, que es como se validan campos en Lua sin bibliotecas.

local function es_hexadecimal(s) return s:match("^%x+$") ~= nil end
print(es_hexadecimal("1f4B"), es_hexadecimal("1g4"))   --> true   false
print(("precio$"):match("precio$"))                    --> nil, el dolar ancla
print(("precio$"):match("precio%$"))                   --> precio$
⚠️
Nunca interpoles datos del usuario sin escapar

Cualquier cadena que venga de fuera y se concatene dentro de un patrón puede contener mágicos. En el mejor de los casos la búsqueda falla; en el peor, un punto convertido en comodín devuelve coincidencias que no debía o un patrón patológico dispara el retroceso. Si la búsqueda es literal, o escapas con la función anterior o usas string.find con su cuarto argumento de búsqueda simple.

⚔️ Agota el vocabulario
  1. Escribe un patrón que reconozca identificadores al estilo de Lua y compruébalo contra nombres que empiecen por dígito, que lleven guion bajo y que lleven acentos.
  2. Demuestra con dos ejemplos que el guion es literal al principio y al final de un conjunto, y rango en medio.
  3. Construye el mismo extractor de texto entrecomillado con el cuantificador codicioso y con el perezoso, y explica exactamente en qué byte se separan sus recorridos.
  4. Implementa «una o más, perezoso» y justifica por qué el lenguaje no lo ofrece como símbolo propio.
  5. Escribe la función de escapado, pásale una cadena con los once mágicos y verifica que el resultado se encuentra a sí mismo con string.find sin búsqueda simple.