nom es una librería de parser combinators para Rust. En lugar de generar un parser desde una gramática (como ANTLR o yacc), en nom compones parsers pequeños y simples para construir parsers más complejos. Cada parser es una función que toma entrada y devuelve la entrada restante junto con el dato parseado.
Es especialmente útil para parsear protocolos binarios, formatos de datos personalizados o lenguajes de configuración.
[dependencies]
nom = "7"
IResult: el tipo de retorno de nom
Todos los parsers de nom devuelven IResult:
pub type IResult<I, O, E = Error<I>> = Result<(I, O), Err<E>>;
// ^^^^^^^^
// (entrada_restante, salida_parseada)
Si el parser tiene éxito, devuelve Ok((resto, valor)) donde resto es la parte de la entrada que no se ha consumido y valor es el dato parseado. Si falla, devuelve un error.
Parsers básicos
use nom::{
bytes::complete::{tag, take, take_while},
character::complete::{alpha1, digit1, alphanumeric1, char, space0, space1},
IResult,
};
// tag: coincide con un string literal exacto
fn parsear_hola(input: &str) -> IResult<&str, &str> {
tag("hola")(input)
}
// digit1: uno o más dígitos
fn parsear_numero(input: &str) -> IResult<&str, &str> {
digit1(input)
}
fn main() {
println!("{:?}", parsear_hola("hola mundo"));
// Ok((" mundo", "hola"))
println!("{:?}", parsear_numero("123abc"));
// Ok(("abc", "123"))
println!("{:?}", parsear_hola("adios"));
// Err(...)
}
Combinadores: componer parsers
La potencia de nom está en los combinadores, que toman parsers como argumento y devuelven parsers más complejos:
use nom::{
branch::alt,
combinator::{map, opt, value},
multi::{many0, many1, separated_list0},
sequence::{preceded, terminated, delimited, pair, tuple},
IResult,
};
// pair: ejecuta dos parsers en secuencia, devuelve tupla
fn clave_valor(input: &str) -> IResult<&str, (&str, &str)> {
pair(
alphanumeric1,
preceded(char('='), alphanumeric1),
)(input)
}
// alt: prueba alternativas en orden, devuelve la primera que funciona
fn booleano(input: &str) -> IResult<&str, bool> {
alt((
value(true, tag("true")),
value(false, tag("false")),
))(input)
}
// many1: repite un parser una o más veces
fn lista_digitos(input: &str) -> IResult<&str, Vec<&str>> {
many1(preceded(space0, digit1))(input)
}
println!("{:?}", clave_valor("nombre=Ana"));
// Ok(("", ("nombre", "Ana")))
println!("{:?}", booleano("true y algo más"));
// Ok((" y algo más", true))
Un parser de CSV
use nom::{
bytes::complete::is_not,
character::complete::char,
multi::separated_list0,
sequence::delimited,
IResult,
};
// Un campo CSV puede estar entre comillas o no
fn campo_csv(input: &str) -> IResult<&str, &str> {
alt((
delimited(char('"'), is_not("""), char('"')),
is_not(",n"),
))(input)
}
// Una línea CSV es campos separados por comas
fn linea_csv(input: &str) -> IResult<&str, Vec<&str>> {
separated_list0(char(','), campo_csv)(input)
}
// El CSV completo es líneas separadas por newlines
fn csv_completo(input: &str) -> IResult<&str, Vec<Vec<&str>>> {
separated_list0(char('n'), linea_csv)(input)
}
fn main() {
let csv = "nombre,edad,ciudadnAna,30,Madridn"García, José",25,Barcelona";
let (_, datos) = csv_completo(csv).unwrap();
for fila in datos {
println!("{:?}", fila);
}
// ["nombre", "edad", "ciudad"]
// ["Ana", "30", "Madrid"]
// ["García, José", "25", "Barcelona"]
}
Parsear protocolos binarios
nom es especialmente útil para protocolos binarios donde cada campo tiene un tamaño fijo y un encoding específico:
use nom::{
bytes::complete::take,
number::complete::{be_u16, be_u32, le_u32},
IResult,
};
#[derive(Debug)]
struct CabeceraPaquete {
version: u8,
tipo_mensaje: u8,
longitud: u32,
id_sesion: u16,
}
#[derive(Debug)]
struct Paquete<'a> {
cabecera: CabeceraPaquete,
datos: &'a [u8],
}
fn parsear_cabecera(input: &[u8]) -> IResult<&[u8], CabeceraPaquete> {
let (input, cabecera_bytes) = take(8u8)(input)?; // cabecera de 8 bytes
let (_, version) = take(1u8)(cabecera_bytes)?;
let (_, tipo) = take(1u8)(&cabecera_bytes[1..])?;
let (_, longitud) = be_u32(&cabecera_bytes[2..6])?;
let (_, id_sesion) = be_u16(&cabecera_bytes[6..8])?;
Ok((input, CabeceraPaquete {
version: version[0],
tipo_mensaje: tipo[0],
longitud,
id_sesion,
}))
}
fn parsear_paquete(input: &[u8]) -> IResult<&[u8], Paquete> {
let (input, cabecera) = parsear_cabecera(input)?;
let longitud = cabecera.longitud as usize;
let (input, datos) = take(longitud)(input)?;
Ok((input, Paquete { cabecera, datos }))
}
map y map_res: transformar resultados
El combinador map transforma el valor producido por un parser. map_res permite transformaciones que pueden fallar:
use nom::{
character::complete::digit1,
combinator::{map, map_res},
IResult,
};
// Parsear un número y convertirlo a u64
fn numero_u64(input: &str) -> IResult<&str, u64> {
map_res(digit1, |s: &str| s.parse::<u64>())(input)
}
// Parsear "true"/"false" a bool
fn booleano_v2(input: &str) -> IResult<&str, bool> {
map(
alt((tag("true"), tag("false"))),
|s: &str| s == "true",
)(input)
}
println!("{:?}", numero_u64("42 abc")); // Ok((" abc", 42u64))
println!("{:?}", booleano_v2("false!")); // Ok(("!", false))
Gestión de errores en nom
Por defecto nom usa errores básicos. Para mensajes de error más informativos, usa el tipo VerboseError:
use nom::{
error::{VerboseError, convert_error},
Err,
};
fn parsear_con_error(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
nom::bytes::complete::tag::<_, _, VerboseError<&str>>("hola")(input)
}
match parsear_con_error("adios") {
Err(Err::Error(e)) | Err(Err::Failure(e)) => {
println!("Error:n{}", convert_error("adios", e));
}
_ => {}
}
nom es la herramienta estándar en el ecosistema Rust para parsear datos binarios y formatos personalizados. Para texto con gramáticas más complejas, también existen pest (que usa una gramática PEG) y chumsky (parser combinators con mejor manejo de errores). Cada uno tiene su caso de uso ideal.
