Rust

Parsing e Processamento de Texto — nom, pest e Expressões Regulares Já leu

18 min de leitura

Parsing e Processamento de Texto — nom, pest e Expressões Regulares
Parsing é uma das tarefas mais universais da programação. Toda vez que você lê um arquivo de configuração, processa um log, interpreta um protocolo, ou implementa uma linguagem, você está fazendo parsing. Rust tem um eco

Parsing é uma das tarefas mais universais da programação. Toda vez que você lê um arquivo de configuração, processa um log, interpreta um protocolo, ou implementa uma linguagem, você está fazendo parsing. Rust tem um ecossistema rico para isso: desde expressões regulares para padrões simples, até combinadores de parser para gramáticas complexas.

Neste artigo cobrimos três abordagens em ordem crescente de poder e complexidade: expressões regulares com regex, parsing manual com técnicas idiomáticas de Rust, e parsers completos com nom e pest.


Processamento de strings — o arsenal básico

Antes de partir para bibliotecas externas, Rust oferece métodos poderosos nativamente:

fn demonstrar_metodos_string() {
    let texto = "  Rust é uma linguagem de sistemas rápida e segura.  ";

    // Limpeza
    println!("{:?}", texto.trim());
    println!("{:?}", texto.trim_start());
    println!("{:?}", texto.trim_end());

    // Busca
    println!("{}", texto.contains("Rust"));           // true
    println!("{:?}", texto.find("linguagem"));         // Some(16)
    println!("{:?}", texto.rfind('a'));                 // Some(47)
    println!("{}", texto.starts_with("  Rust"));       // true
    println!("{}", texto.ends_with("segura.  "));      // true

    // Transformação
    println!("{}", texto.to_uppercase());
    println!("{}", texto.to_lowercase());
    println!("{}", texto.replace("rápida", "veloz"));
    println!("{}", texto.replacen("a", "X", 3));

    // Divisão
    let palavras: Vec<&str> = texto.split_whitespace().collect();
    println!("{:?}", palavras);

    let partes: Vec<&str> = "a,b,,c,".split(',').collect();
    println!("{:?}", partes); // ["a", "b", "", "c", ""]

    let partes_filtradas: Vec<&str> = "a,b,,c,"
        .split(',')
        .filter(|s| !s.is_empty())
        .collect();
    println!("{:?}", partes_filtradas); // ["a", "b", "c"]

    // Splitn — limita o número de partes
    let partes: Vec<&str> = "a:b:c:d".splitn(3, ':').collect();
    println!("{:?}", partes); // ["a", "b", "c:d"]

    // Parsing de números
    let n: i32 = "42".parse().unwrap();
    let f: f64 = "3.14".parse().unwrap();
    println!("{n} {f}");

    // chars() para iteração Unicode-correta
    let emoji = "olá 🦀 mundo";
    println!("Chars: {}", emoji.chars().count()); // 11, não bytes
    println!("Bytes: {}", emoji.len());            // 15 bytes
}

fn main() {
    demonstrar_metodos_string();
}

Expressões regulares com regex

Para padrões que os métodos nativos não alcançam, regex é a solução:

[dependencies]
regex = "1"
once_cell = "1"
use once_cell::sync::Lazy;
use regex::Regex;

// Compilar regex é custoso — compile uma vez e reutilize
static RE_EMAIL: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}").unwrap()
});

static RE_CPF: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"d{3}.?d{3}.?d{3}-?d{2}").unwrap()
});

static RE_URL: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"https?://[^s<>"]+").unwrap()
});

static RE_DATA: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(d{2})/(d{2})/(d{4})").unwrap()
});

fn extrair_informacoes(texto: &str) {
    println!("── Texto ──
{texto}
");

    // Verificar se contém padrão
    if RE_EMAIL.is_match(texto) {
        println!("✓ Contém email");
    }

    // Encontrar primeiro match
    if let Some(m) = RE_EMAIL.find(texto) {
        println!("Primeiro email: {}", m.as_str());
        println!("  Posição: {}..{}", m.start(), m.end());
    }

    // Encontrar todos os matches
    let emails: Vec<&str> = RE_EMAIL
        .find_iter(texto)
        .map(|m| m.as_str())
        .collect();
    println!("Todos os emails: {:?}", emails);

    // Capturar grupos
    for caps in RE_DATA.captures_iter(texto) {
        println!(
            "Data: {}/{}/{} (dia/mês/ano)",
            &caps[1], &caps[2], &caps[3]
        );
    }

    // URLs
    let urls: Vec<&str> = RE_URL
        .find_iter(texto)
        .map(|m| m.as_str())
        .collect();
    println!("URLs: {:?}", urls);
}

fn substituicoes_avancadas(texto: &str) -> String {
    // Substituição simples
    let re = Regex::new(r"(w+)s+1").unwrap(); // palavras duplicadas
    let sem_duplicatas = re.replace_all(texto, "$1");

    // Substituição com closure — transforma cada match
    let re_numero = Regex::new(r"d+").unwrap();
    let numeros_dobrados = re_numero.replace_all(
        &sem_duplicatas,
        |caps: &regex::Captures| {
            let n: u64 = caps[0].parse().unwrap_or(0);
            (n * 2).to_string()
        }
    );

    numeros_dobrados.to_string()
}

fn parser_de_log(linha: &str) -> Option<EntradaLog> {
    static RE_LOG: Lazy<Regex> = Lazy::new(|| {
        Regex::new(
            r#"^(d{4}-d{2}-d{2} d{2}:d{2}:d{2}) [(w+)] [([^]]+)] (.+)$"#
        ).unwrap()
    });

    let caps = RE_LOG.captures(linha)?;

    Some(EntradaLog {
        timestamp: caps[1].to_string(),
        nivel:     caps[2].to_string(),
        modulo:    caps[3].to_string(),
        mensagem:  caps[4].to_string(),
    })
}

#[derive(Debug)]
struct EntradaLog {
    timestamp: String,
    nivel:     String,
    modulo:    String,
    mensagem:  String,
}

fn main() {
    let texto = "\
        Contato: ana@exemplo.com e carlos@empresa.org.br
\
        CPF: 123.456.789-09
\
        Nascimento: 15/03/1990
\
        Site: https://www.rust-lang.org e https://crates.io
\
    ";

    extrair_informacoes(texto);

    println!("
── Substituições ──");
    let texto2 = "O rato rato roeu 10 livros livros";
    println!("Antes:  {texto2}");
    println!("Depois: {}", substituicoes_avancadas(texto2));

    println!("
── Parser de Log ──");
    let logs = [
        "2024-03-01 14:23:45 [INFO] [servidor::http] Requisição recebida: GET /api/usuarios",
        "2024-03-01 14:23:46 [ERROR] [servidor::db] Conexão recusada: timeout após 30s",
        "linha inválida",
    ];

    for linha in &logs {
        match parser_de_log(linha) {
            Some(entrada) => println!("{:?}", entrada),
            None          => println!("Linha inválida: {linha}"),
        }
    }
}

Parsing manual com tipos de Rust

Para protocolos simples, parsing manual com iteradores de Rust é eficiente e legível:

#[derive(Debug, PartialEq)]
struct Rgb {
    r: u8,
    g: u8,
    b: u8,
}

#[derive(Debug)]
enum ErroParsing {
    FormatoInvalido(String),
    ValorFora(String),
    EntradaVazia,
}

impl std::fmt::Display for ErroParsing {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ErroParsing::FormatoInvalido(s) => write!(f, "Formato inválido: {s}"),
            ErroParsing::ValorFora(s)       => write!(f, "Valor fora do intervalo: {s}"),
            ErroParsing::EntradaVazia       => write!(f, "Entrada vazia"),
        }
    }
}

impl Rgb {
    // Parseia "#RRGGBB" ou "rgb(R, G, B)"
    fn parse(s: &str) -> Result<Self, ErroParsing> {
        let s = s.trim();

        if s.is_empty() {
            return Err(ErroParsing::EntradaVazia);
        }

        if s.starts_with('#') {
            return Rgb::parse_hex(s);
        }

        if s.starts_with("rgb(") {
            return Rgb::parse_rgb_fn(s);
        }

        Err(ErroParsing::FormatoInvalido(
            format!("Não reconheço o formato: {s}")
        ))
    }

    fn parse_hex(s: &str) -> Result<Self, ErroParsing> {
        let hex = s.trim_start_matches('#');

        if hex.len() != 6 {
            return Err(ErroParsing::FormatoInvalido(
                format!("Hex deve ter 6 caracteres, não {}", hex.len())
            ));
        }

        let parse_canal = |fatiado: &str| -> Result<u8, ErroParsing> {
            u8::from_str_radix(fatiado, 16)
                .map_err(|_| ErroParsing::FormatoInvalido(
                    format!("Caractere hex inválido: {fatiado}")
                ))
        };

        Ok(Rgb {
            r: parse_canal(&hex[0..2])?,
            g: parse_canal(&hex[2..4])?,
            b: parse_canal(&hex[4..6])?,
        })
    }

    fn parse_rgb_fn(s: &str) -> Result<Self, ErroParsing> {
        // "rgb(255, 128, 0)"
        let interior = s
            .strip_prefix("rgb(")
            .and_then(|s| s.strip_suffix(')'))
            .ok_or_else(|| ErroParsing::FormatoInvalido(
                "Esperava rgb(...) ".to_string()
            ))?;

        let componentes: Vec<&str> = interior
            .split(',')
            .map(str::trim)
            .collect();

        if componentes.len() != 3 {
            return Err(ErroParsing::FormatoInvalido(
                format!("Esperava 3 componentes, obtive {}", componentes.len())
            ));
        }

        let parse_canal = |s: &str| -> Result<u8, ErroParsing> {
            let n: u16 = s.parse().map_err(|_| {
                ErroParsing::FormatoInvalido(format!("Não é número: {s}"))
            })?;
            if n > 255 {
                return Err(ErroParsing::ValorFora(
                    format!("{n} > 255")
                ));
            }
            Ok(n as u8)
        };

        Ok(Rgb {
            r: parse_canal(componentes[0])?,
            g: parse_canal(componentes[1])?,
            b: parse_canal(componentes[2])?,
        })
    }

    fn to_hex(&self) -> String {
        format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
    }

    fn luminancia(&self) -> f64 {
        0.299 * self.r as f64 / 255.0
            + 0.587 * self.g as f64 / 255.0
            + 0.114 * self.b as f64 / 255.0
    }
}

fn main() {
    let entradas = [
        "#FF5733",
        "#abc123",
        "rgb(255, 128, 0)",
        "rgb(0, 0, 0)",
        "rgb(256, 0, 0)",  // inválido
        "#GGGGGG",         // inválido
        "azul",            // inválido
    ];

    for entrada in &entradas {
        match Rgb::parse(entrada) {
            Ok(cor) => println!(
                "{entrada:20} → r={:3} g={:3} b={:3}  hex={}  lum={:.3}",
                cor.r, cor.g, cor.b, cor.to_hex(), cor.luminancia()
            ),
            Err(e) => println!("{entrada:20} → ERRO: {e}"),
        }
    }
}

Saída:

#FF5733              → r=255 g= 87 b= 51  hex=#FF5733  lum=0.457
#abc123              → r=171 g=193 b= 35  hex=#ABC123  lum=0.645
rgb(255, 128, 0)     → r=255 g=128 b=  0  hex=#FF8000  lum=0.375
rgb(0, 0, 0)         → r=  0 g=  0 b=  0  hex=#000000  lum=0.000
rgb(256, 0, 0)       → ERRO: Valor fora do intervalo: 256 > 255
#GGGGGG              → ERRO: Formato inválido: Caractere hex inválido: GG
azul                 → ERRO: Formato inválido: Não reconheço o formato: azul

nom — combinadores de parser

nom é uma biblioteca de parser combinadores — você constrói parsers complexos combinando parsers simples. É zero-copy, extremamente eficiente, e ideal para protocolos binários e textuais:

[dependencies]
nom = "7"
use nom::{
    branch::alt,
    bytes::complete::{tag, tag_no_case, take_while, take_while1},
    character::complete::{char, digit1, multispace0, multispace1},
    combinator::{map, map_res, opt, value},
    multi::separated_list0,
    sequence::{delimited, preceded, separated_pair, terminated, tuple},
    IResult,
};

// Tipo de resultado do nom: (input_restante, valor_parseado)
// IResult<I, O> = Result<(I, O), Err<...>>

// Parser para inteiro sem sinal
fn parsear_u64(input: &str) -> IResult<&str, u64> {
    map_res(digit1, str::parse)(input)
}

// Parser para número com sinal
fn parsear_i64(input: &str) -> IResult<&str, i64> {
    map_res(
        tuple((opt(char('-')), digit1)),
        |(sinal, digitos): (Option<char>, &str)| {
            let n: i64 = digitos.parse()?;
            Ok::<i64, std::num::ParseIntError>(if sinal.is_some() { -n } else { n })
        }
    )(input)
}

// Parser para identificador: [a-zA-Z_][a-zA-Z0-9_]*
fn parsear_ident(input: &str) -> IResult<&str, &str> {
    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
}

// Parser para string entre aspas simples
fn parsear_string(input: &str) -> IResult<&str, &str> {
    delimited(
        char('"'),
        take_while(|c| c != '"'),
        char('"')
    )(input)
}

// Parser de valor JSON simplificado
#[derive(Debug, PartialEq)]
enum ValorJson {
    Nulo,
    Bool(bool),
    Numero(f64),
    Texto(String),
    Array(Vec<ValorJson>),
    Objeto(Vec<(String, ValorJson)>),
}

fn espaco(input: &str) -> IResult<&str, &str> {
    multispace0(input)
}

fn parsear_json(input: &str) -> IResult<&str, ValorJson> {
    preceded(espaco, alt((
        parsear_json_nulo,
        parsear_json_bool,
        parsear_json_numero,
        parsear_json_texto,
        parsear_json_array,
        parsear_json_objeto,
    )))(input)
}

fn parsear_json_nulo(input: &str) -> IResult<&str, ValorJson> {
    value(ValorJson::Nulo, tag("null"))(input)
}

fn parsear_json_bool(input: &str) -> IResult<&str, ValorJson> {
    alt((
        value(ValorJson::Bool(true),  tag("true")),
        value(ValorJson::Bool(false), tag("false")),
    ))(input)
}

fn parsear_json_numero(input: &str) -> IResult<&str, ValorJson> {
    map_res(
        take_while1(|c: char| c.is_ascii_digit() || c == '.' || c == '-'),
        |s: &str| s.parse::<f64>().map(ValorJson::Numero)
    )(input)
}

fn parsear_json_texto(input: &str) -> IResult<&str, ValorJson> {
    map(
        delimited(char('"'), take_while(|c| c != '"'), char('"')),
        |s: &str| ValorJson::Texto(s.to_string())
    )(input)
}

fn parsear_json_array(input: &str) -> IResult<&str, ValorJson> {
    map(
        delimited(
            terminated(char('['), espaco),
            separated_list0(
                delimited(espaco, char(','), espaco),
                parsear_json
            ),
            preceded(espaco, char(']'))
        ),
        ValorJson::Array
    )(input)
}

fn parsear_json_objeto(input: &str) -> IResult<&str, ValorJson> {
    map(
        delimited(
            terminated(char('{'), espaco),
            separated_list0(
                delimited(espaco, char(','), espaco),
                separated_pair(
                    map(
                        delimited(char('"'), take_while(|c| c != '"'), char('"')),
                        str::to_string
                    ),
                    delimited(espaco, char(':'), espaco),
                    parsear_json
                )
            ),
            preceded(espaco, char('}'))
        ),
        ValorJson::Objeto
    )(input)
}

fn main() {
    let entradas = [
        r#"null"#,
        r#"true"#,
        r#"42"#,
        r#"3.14"#,
        r#""hello world""#,
        r#"[1, 2, 3]"#,
        r#"{"nome": "Ana", "idade": 30}"#,
        r#"{"lista": [1, true, null, "texto"]}"#,
    ];

    for entrada in &entradas {
        match parsear_json(entrada) {
            Ok(("", valor)) => println!("{entrada:45} → {:?}", valor),
            Ok((resto, valor)) => println!("{entrada:45} → {:?} (resto: {:?})", valor, resto),
            Err(e) => println!("{entrada:45} → ERRO: {e}"),
        }
    }
}

Saída:

null                                          → Nulo
true                                          → Bool(true)
42                                            → Numero(42.0)
3.14                                          → Numero(3.14)
"hello world"                                 → Texto("hello world")
[1, 2, 3]                                     → Array([Numero(1.0), Numero(2.0), Numero(3.0)])
{"nome": "Ana", "idade": 30}                  → Objeto([("nome", Texto("Ana")), ("idade", Numero(30.0))])
{"lista": [1, true, null, "texto"]}           → Objeto([("lista", Array([...]))])

pest — parsing com gramáticas PEG

pest usa gramáticas PEG (Parsing Expression Grammars) definidas em arquivos .pest. É mais legível para gramáticas complexas:

[dependencies]
pest = "2"
pest_derive = "2"

src/gramatica.pest:

// Regras de whitespace e comentários (ignorados automaticamente)
WHITESPACE = _{ " " | "	" | "
" | "
" }
COMMENT    = _{ "//" ~ (!"
" ~ ANY)* ~ "
" }

// Tipos primitivos
inteiro = @{ "-"? ~ ASCII_DIGIT+ }
flutuante = @{ "-"? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
booleano = { "true" | "false" }
nulo = { "null" }

// String
string_char = _{ !(""" | "\") ~ ANY | "\" ~ ANY }
string = @{ """ ~ string_char* ~ """ }

// Identificador
ident = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }

// Expressões
valor = { flutuante | inteiro | booleano | nulo | string | lista | objeto }

// Lista
lista = { "[" ~ (valor ~ ("," ~ valor)*)? ~ "]" }

// Objeto (pares chave-valor)
par = { (string | ident) ~ ":" ~ valor }
objeto = { "{" ~ (par ~ ("," ~ par)*)? ~ "}" }

// Regra de entrada completa
programa = { SOI ~ valor ~ EOI }

// Declaração de variável (para demo de linguagem simples)
declaracao = { "let" ~ ident ~ "=" ~ valor ~ ";" }
bloco = { SOI ~ declaracao* ~ EOI }

src/main.rs:

use pest::Parser;
use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "src/gramatica.pest"]
struct MeuParser;

fn parsear_com_pest(entrada: &str) {
    match MeuParser::parse(Rule::programa, entrada) {
        Ok(pares) => {
            println!("Parse bem-sucedido!");
            imprimir_arvore(pares, 0);
        }
        Err(e) => {
            println!("Erro de parse:
{e}");
        }
    }
}

fn imprimir_arvore(pares: pest::iterators::Pairs<Rule>, profundidade: usize) {
    for par in pares {
        let indent = "  ".repeat(profundidade);
        let regra = format!("{:?}", par.as_rule());
        let texto = par.as_str();

        if par.clone().into_inner().next().is_some() {
            println!("{indent}{regra}: {texto:?}");
            imprimir_arvore(par.into_inner(), profundidade + 1);
        } else {
            println!("{indent}{regra}: {texto:?}");
        }
    }
}

fn main() {
    let entradas = [
        r#"42"#,
        r#"{"nome": "Rust", "versao": 2021, "estavel": true}"#,
        r#"[1, 2.5, "texto", null]"#,
    ];

    for entrada in &entradas {
        println!("── Parseando: {entrada:?} ──");
        parsear_com_pest(entrada);
        println!();
    }
}

Um programa completo: parser de configuração

Vamos construir um parser para um formato de configuração customizado usando nom:

// Formato:
// # comentário
// [seção]
// chave = valor
// chave = "valor com espaços"
// chave = 42
// chave = true

use nom::{
    branch::alt,
    bytes::complete::{tag, take_till, take_while, take_while1},
    character::complete::{char, line_ending, not_line_ending, space0, space1},
    combinator::{map, map_res, opt, value},
    multi::many0,
    sequence::{delimited, preceded, terminated, tuple},
    IResult,
};
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq)]
enum Valor {
    Texto(String),
    Inteiro(i64),
    Flutuante(f64),
    Booleano(bool),
}

impl std::fmt::Display for Valor {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Valor::Texto(s)    => write!(f, "{s}"),
            Valor::Inteiro(n)  => write!(f, "{n}"),
            Valor::Flutuante(x)=> write!(f, "{x}"),
            Valor::Booleano(b) => write!(f, "{b}"),
        }
    }
}

type Config = HashMap<String, HashMap<String, Valor>>;

fn comentario(input: &str) -> IResult<&str, ()> {
    value(
        (),
        tuple((
            char('#'),
            not_line_ending,
            opt(line_ending),
        ))
    )(input)
}

fn linha_vazia(input: &str) -> IResult<&str, ()> {
    value((), tuple((space0, opt(line_ending))))(input)
}

fn nome_secao(input: &str) -> IResult<&str, &str> {
    delimited(
        char('['),
        take_while1(|c: char| c != ']' && c != '
'),
        char(']'),
    )(input)
}

fn chave(input: &str) -> IResult<&str, &str> {
    take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-')(input)
}

fn valor_texto(input: &str) -> IResult<&str, Valor> {
    map(
        delimited(
            char('"'),
            take_while(|c| c != '"'),
            char('"'),
        ),
        |s: &str| Valor::Texto(s.to_string())
    )(input)
}

fn valor_booleano(input: &str) -> IResult<&str, Valor> {
    alt((
        value(Valor::Booleano(true),  tag("true")),
        value(Valor::Booleano(false), tag("false")),
    ))(input)
}

fn valor_numero(input: &str) -> IResult<&str, Valor> {
    let (resto, texto) = take_while1(|c: char| {
        c.is_ascii_digit() || c == '.' || c == '-'
    })(input)?;

    if texto.contains('.') {
        let n: f64 = texto.parse()
            .map_err(|_| nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::Float
            )))?;
        Ok((resto, Valor::Flutuante(n)))
    } else {
        let n: i64 = texto.parse()
            .map_err(|_| nom::Err::Error(nom::error::Error::new(
                input,
                nom::error::ErrorKind::Digit
            )))?;
        Ok((resto, Valor::Inteiro(n)))
    }
}

fn valor_inline(input: &str) -> IResult<&str, Valor> {
    alt((
        valor_booleano,
        valor_texto,
        valor_numero,
    ))(input)
}

fn par_chave_valor(input: &str) -> IResult<&str, (&str, Valor)> {
    tuple((
        terminated(chave, tuple((space0, char('='), space0))),
        terminated(valor_inline, tuple((space0, opt(line_ending)))),
    ))(input)
}

fn parsear_config(input: &str) -> Result<Config, String> {
    let mut config: Config = HashMap::new();
    let mut secao_atual = "global".to_string();
    config.insert(secao_atual.clone(), HashMap::new());

    let mut resto = input;

    while !resto.is_empty() {
        // Tenta pular comentário
        if let Ok((novo_resto, _)) = comentario(resto) {
            resto = novo_resto;
            continue;
        }

        // Tenta pular linha vazia
        if let Ok((novo_resto, _)) = linha_vazia(resto) {
            if novo_resto.len() < resto.len() {
                resto = novo_resto;
                continue;
            }
        }

        // Tenta parsear cabeçalho de seção
        if let Ok((novo_resto, nome)) = terminated(
            preceded(space0, nome_secao),
            tuple((space0, opt(line_ending)))
        )(resto) {
            secao_atual = nome.trim().to_string();
            config.entry(secao_atual.clone()).or_default();
            resto = novo_resto;
            continue;
        }

        // Tenta parsear par chave=valor
        if let Ok((novo_resto, (chave, valor))) = preceded(space0, par_chave_valor)(resto) {
            config
                .entry(secao_atual.clone())
                .or_default()
                .insert(chave.to_string(), valor);
            resto = novo_resto;
            continue;
        }

        // Linha não reconhecida — pula
        if let Ok((novo_resto, _)) = not_line_ending::<&str, ()>(resto) {
            let _ = opt(line_ending::<&str, ()>)(novo_resto);
            break;
        } else {
            break;
        }
    }

    Ok(config)
}

fn exibir_config(config: &Config) {
    let mut secoes: Vec<&String> = config.keys().collect();
    secoes.sort();

    for secao in secoes {
        println!("[{secao}]");
        let mut pares: Vec<(&String, &Valor)> = config[secao].iter().collect();
        pares.sort_by_key(|(k, _)| k.as_str());

        for (chave, valor) in pares {
            let tipo = match valor {
                Valor::Texto(_)    => "texto",
                Valor::Inteiro(_)  => "inteiro",
                Valor::Flutuante(_)=> "flutuante",
                Valor::Booleano(_) => "booleano",
            };
            println!("  {chave:20} = {valor:15} ({tipo})");
        }
        println!();
    }
}

fn main() {
    let config_texto = r#"
# Configuração da aplicação
# Gerado automaticamente

[servidor]
host = "0.0.0.0"
porta = 8080
debug = false
max_conexoes = 1000
timeout = 30.5

[banco]
host = "localhost"
porta = 5432
nome = "producao"
ssl = true
pool_min = 2
pool_max = 20

[cache]
ativo = true
ttl = 3600
max_entradas = 10000
prefixo = "app_v2"

[log]
nivel = "info"
arquivo = "/var/log/app.log"
rotacao = true
max_tamanho_mb = 100
"#;

    match parsear_config(config_texto) {
        Ok(config) => {
            println!("Config parseada com sucesso!
");
            exibir_config(&config);

            // Acesso a valores específicos
            if let Some(servidor) = config.get("servidor") {
                if let Some(Valor::Inteiro(porta)) = servidor.get("porta") {
                    println!("Porta do servidor: {porta}");
                }
                if let Some(Valor::Texto(host)) = servidor.get("host") {
                    println!("Host do servidor: {host}");
                }
            }
        }
        Err(e) => eprintln!("Erro ao parsear config: {e}"),
    }
}

Saída:

Config parseada com sucesso!

[banco]
  host                 = localhost       (texto)
  max                  = 20             (inteiro)
  min                  = 2              (inteiro)
  nome                 = producao       (texto)
  porta                = 5432           (inteiro)
  ssl                  = true           (booleano)

[cache]
  ativo                = true           (booleano)
  max_entradas         = 10000          (inteiro)
  prefixo              = app_v2         (texto)
  ttl                  = 3600           (inteiro)

[log]
  arquivo              = /var/log/app.log (texto)
  max_tamanho_mb       = 100           (inteiro)
  nivel                = info           (texto)
  rotacao              = true           (booleano)

[servidor]
  debug                = false          (booleano)
  host                 = 0.0.0.0        (texto)
  max_conexoes         = 1000           (inteiro)
  porta                = 8080           (inteiro)
  timeout              = 30.5           (flutuante)

Porta do servidor: 8080
Host do servidor: 0.0.0.0

Quando usar cada abordagem

A escolha entre regex, parsing manual, nom e pest depende da complexidade da gramática:

Use métodos nativos de string para divisões simples, prefixos e sufixos conhecidos. São rápidos, sem dependências, e suficientes para a maioria dos casos.

Use regex para padrões que seriam verbosos de expressar manualmente — emails, URLs, logs com formato variável. Compile sempre com Lazy ou OnceLock para não recompilar a cada chamada.

Use parsing manual para protocolos simples com estrutura previsível. O código Rust idiomático com split, strip_prefix e parse é frequentemente mais claro que uma regex complexa.

Use nom para protocolos binários ou textuais com gramática moderada a complexa, onde performance é crítica e você quer zero-copy. A curva de aprendizado é íngreme mas o resultado é eficiente e composável.

Use pest quando a gramática é complexa e você quer documentá-la de forma legível. A gramática PEG em arquivo separado funciona como documentação viva do protocolo.


Fontes e leituras recomendadas

  • regex crate documentation — https://docs.rs/regex
  • nom documentation — https://docs.rs/nom
  • nom — Writing Parsers — guia oficial com exemplos progressivos — https://github.com/rust-bakery/nom/blob/main/doc/making_a_new_parser_from_scratch.md
  • pest documentation e book — https://pest.rs
  • "Parse, don't validate" — Alexis King — artigo filosófico fundamental sobre parsing — https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
  • winnow crate — fork moderno do nom com melhor ergonomia — https://docs.rs/winnow
  • chumsky crate — parser combinadores com recuperação de erros — https://docs.rs/chumsky
Comentários

Mais em Rust

Criptografia e Segurança — Hashing, Criptografia Simétrica e TLS
Criptografia e Segurança — Hashing, Criptografia Simétrica e TLS

Segurança não é uma camada adicional que se adiciona ao software — é uma prop...

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu
Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu

&nbsp; Se structs s&atilde;o a forma de Rust agrupar dados relacionados,&nbs...

Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade
Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade

Rust — Artigo #49 Contribuindo com o Ecossistema Rust — Open Source, RFCs e a...