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: ®ex::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
regexcrate documentation — https://docs.rs/regexnomdocumentation — https://docs.rs/nomnom— Writing Parsers — guia oficial com exemplos progressivos — https://github.com/rust-bakery/nom/blob/main/doc/making_a_new_parser_from_scratch.mdpestdocumentation 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/
winnowcrate — fork moderno do nom com melhor ergonomia — https://docs.rs/winnowchumskycrate — parser combinadores com recuperação de erros — https://docs.rs/chumsky