grep es la navaja suiza del linuxero. Buscas texto en archivos constantemente: logs, configuraciones, código fuente, salida de comandos… Y aunque grep de GNU es potente, tiene sus limitaciones. ¿Y si pudieras extenderlo, personalizarlo, o simplemente entender cómo funciona por dentro?. Desde luego que ya existen herramientas realmente potentes que te permiten hacerlo, sin embargo, ¿no te parece que es una forma excelente de exprimir Rust para Linuxeros?. En este caso, y con grep en mente, vamos a ver distintos aspectos que son realmente interesantes como la búsqueda recursiva en directorios, el resaltado de coincidencias utilizando colores, número de líneas, conteo de coincidencias y mas.
Como vamos a realizar nuestro grep
Para hacer este grep en Rust para Linuxeros, utilizaremos determinados crates de Rust que hacen el trabajo pesado por ti, clap para los argumentos CLI, walkdir para navegar el sistema de archivos, regex para los patrones, colored para los colores, y anyhow para los errores.
Si vienes de Bash, esto te sonará:
grep -rin "error" /var/log/ 2>/dev/null
Pues en Rust vas a construir una herramienta que hace exactamente eso, pero que puedes extender como quieras, compila a un binario estático, y no depende de si el sistema tiene GNU grep, BSD grep, o grep con puto –color=auto.
Vamos a ello.
El proyecto: crustaceo-grep
Como en los capítulos anteriores, creamos un proyecto nuevo con Cargo:
cargo new crustaceo-grep
cd crustaceo-grep
Edita Cargo.toml con estas dependencias:
[package]
name = "crustaceo-grep"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4", features = ["derive"] }
regex = "1"
walkdir = "2"
colored = "2"
anyhow = "1"
Vamos a ver qué hace cada una:
- clap 4.x — la crate definitiva para construir CLI tools profesionales. Soporta subcomandos, autocompletado, mensajes de ayuda automáticos, y la macro derive que nos ahorra escribir boilerplate. Con la feature
derivehabilitada, defines los argumentos como structs de Rust, y clap genera el parseo, la validación y el mensaje--helpautomáticamente. - regex — expresiones regulares compiladas a código máquina. Más rápida que la regex de casi cualquier otro lenguaje porque internamente usa la librería
regex-automatade Rust, que construye un DFA (autómata finito determinista) y elimina backtracking. - walkdir — iterador recursivo de directorios. Te permite recorrer árboles de archivos con control sobre qué entra y qué no (simlinks, permisos, profundidad máxima). Es lo que usarías en Bash con
find . -type f. - colored — ya la conoces de capítulos anteriores. Añade métodos como
.red(),.green(),.bold()a los strings. - anyhow — manejo de errores simplificado. El
anyhow::Result<T>permite devolver cualquier error sin especificar el tipo exacto.
Argumentos CLI con clap 4
Vamos directos al código. clap con derive es increíblemente limpio. Abre src/main.rs:
use anyhow::{Context, Result};
use clap::Parser;
/// Busca texto en archivos con expresiones regulares
#[derive(Parser)]
#[command(
name = "crustaceo-grep",
version,
about = "grep en Rust con colores, recursividad y regex",
long_about = "crustaceo-grep busca patrones de texto en archivos, \
con soporte para expresiones regulares, búsqueda recursiva, \
coloreado de coincidencias y conteo."
)]
struct Args {
/// Patrón de búsqueda (expresión regular)
patron: String,
/// Archivo(s) a buscar
archivos: Vec<String>,
/// Búsqueda recursiva en directorios
#[arg(short = 'r', long = "recursive")]
recursivo: bool,
/// Ignorar mayúsculas/minúsculas
#[arg(short = 'i', long = "ignore-case")]
ignorar_mayusculas: bool,
/// Solo mostrar el conteo de coincidencias por archivo
#[arg(short = 'c', long = "count")]
solo_conteo: bool,
/// Mostrar número de línea
#[arg(short = 'n', long = "line-number")]
num_linea: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
// Si no hay archivos y no es recursivo, error
if args.archivos.is_empty() && !args.recursivo {
anyhow::bail!("Necesitas especificar al menos un archivo o usar --recursive");
}
println!("Patrón: {}", args.patron);
println!("Recursivo: {}", args.recursivo);
println!("Ignore case: {}", args.ignorar_mayusculas);
println!("Archivos: {:?}", args.archivos);
Ok(())
}
Compila y prueba el parseo de argumentos antes de seguir:
cargo run -- "error" /var/log/syslog
cargo run -- --help
La salida del --help generado por clap es automática:
Busca texto en archivos con expresiones regulares
Usage: crustaceo-grep [OPTIONS] <PATRON> [ARCHIVOS]...
Arguments:
<PATRON> Patrón de búsqueda (expresión regular)
[ARCHIVOS]... Archivo(s) a buscar
Options:
-r, --recursive Búsqueda recursiva en directorios
-i, --ignore-case Ignorar mayúsculas/minúsculas
-c, --count Solo mostrar el conteo de coincidencias por archivo
-n, --line-number Mostrar número de línea
-h, --help Print help
-V, --version Print version
Sin escribir una línea de parseo manual, tienes validación, tipos, mensajes de error y help completo. La macro #[derive(Parser)] examina la estructura, los tipos, las anotaciones #[arg(...)] y los doc comments, y genera todo el código de parseo en tiempo de compilación.
Cómo funciona clap derive
#[derive(Parser)] implementa automáticamente el trait Parser para tu struct. Cada campo se convierte en un argumento:
- Campos sin
#[arg()]→ argumentos posicionales (en el orden en que aparecen) #[arg(short = 'r', long = "recursive")]→ flag-r/--recursive(booleano)Vec<String>→ acepta múltiples valores- El doc comment (
///) de la struct → la descripción del programa - El doc comment de cada campo → la descripción de ese argumento en
--help
Puedes personalizar muchísimo: valores por defecto, valores permitidos, alias, argumentos mutuamente excluyentes… Pero con lo que llevamos tienes suficiente para este proyecto.
Búsqueda recursiva con walkdir
Ahora toca recorrer directorios. walkdir es un iterador que entra en subdirectorios automáticamente y te da rutas completas, metadatos y control sobre errores.
Añade esta función a src/main.rs:
use std::path::Path;
use walkdir::WalkDir;
/// Recorre un directorio recursivamente y devuelve los archivos regulares
fn archivos_recursivos(dir: &Path) -> Vec<String> {
WalkDir::new(dir)
.into_iter()
.filter_map(|e| match e {
Ok(entry) => {
if entry.file_type().is_file() {
Some(entry.path().to_string_lossy().to_string())
} else {
None
}
}
Err(_) => None, // ignora directorios sin permisos
})
.collect()
}
WalkDir::new(dir) crea el iterador. .into_iter() lo consume dando DirEntry o errores. .filter_map() nos permite quedarnos solo con los archivos regulares (is_file()) y descartar errores de permisos silenciosamente — justo lo que hace grep -r de forma predeterminada.
Para limitar la profundidad o seguir simlinks, walkdir ofrece métodos como .max_depth(3) o .follow_links(true). Pero para nuestro grep básico, la configuración por defecto está bien.
Integra esto en main():
fn main() -> Result<()> {
let args = Args::parse();
// Determinar la lista de archivos a procesar
let archivos: Vec<String> = if args.recursivo {
if args.archivos.is_empty() {
// Sin argumentos + recursivo = desde el directorio actual
archivos_recursivos(Path::new("."))
} else {
// Cada argumento puede ser un archivo o un directorio
let mut todos = Vec::new();
for ruta in &args.archivos {
let p = Path::new(ruta);
if p.is_dir() {
todos.extend(archivos_recursivos(p));
} else {
todos.push(ruta.clone());
}
}
todos
}
} else {
args.archivos.clone()
};
// Si no hay archivos que procesar, salir con código 1
if archivos.is_empty() {
eprintln!("No hay archivos que procesar");
std::process::exit(1);
}
println!("Patrón: {}", args.patron);
println!("Archivos a buscar: {}", archivos.len());
Ok(())
}
Ahora cargo run -- -r . ya lista todos los archivos del directorio actual recursivamente.
Expresiones regulares con la crate regex
La crate regex de Rust es una maravilla. Compila el patrón una vez (no por cada línea), y la compilación genera código máquina optimizado. En benchmarks, suele ser más rápida que grep -E para patrones complejos.
La API básica:
use regex::Regex;
let re = Regex::new(r"error\d{3}")?;
let hay_coincidencia = re.is_match("error404");
Pero para nuestro grep necesitamos más: encontrar todas las coincidencias en una línea, con sus posiciones para poder colorearlas. Ahí entra find_iter():
for match_ in re.find_iter("2024 ERROR: timeout en conexión") {
println!(" Coincidencia: '{}' en posición {}", match_.as_str(), match_.start());
}
Cada match devuelto por find_iter() sabe dónde empieza y termina, y qué texto capturó. Eso nos permite segmentar la línea en partes: antes de la coincidencia, la coincidencia misma, después de la coincidencia. Y colorear solo la coincidencia.
Ignorar mayúsculas
Para el flag -i, la crate regex ofrece RegexBuilder que permite configurar opciones antes de compilar el patrón:
use regex::RegexBuilder;
let re = RegexBuilder::new(r"error")
.case_insensitive(true)
.build()?;
Es más eficiente que hacer (?i)error inline, porque es más legible y la configuración está clara desde el principio.
Coloreado de coincidencias con colored
La crate colored te permite pintar partes de un string con métodos encadenados:
use colored::*;
println!("{}", "coincidencia".red().bold());
Pero ojo: colored solo funciona si la salida es una terminal (TTY). Cuando rediriges a un archivo o a un pipe, los códigos ANSI se ven feos. El grep de GNU tiene --color=auto que detecta si la salida es una TTY. Nosotros haremos lo mismo.
fn es_tty() -> bool {
atty::is(atty::Stream::Stdout)
}
Pero para no añadir otra dependencia, podemos usar un método más directo. colored ya respeta la variable de entorno NO_COLOR y CLICOLOR_FORCE. Pero para nuestro caso, vamos a implementar la detección manual con la función is_terminal() de la stdlib reciente:
use std::io::IsTerminal;
fn esta_coloreado() -> bool {
std::io::stdout().is_terminal()
}
A partir de Rust 1.70, std::io::IsTerminal está en la stdlib. Si es terminal, coloreamos; si no, devolvemos texto plano.
Construcción de crustaceo-grep
Ya tenemos todos los ingredientes. Vamos a montar la versión completa de src/main.rs:
use anyhow::{Context, Result};
use clap::Parser;
use colored::*;
use regex::RegexBuilder;
use std::fs::File;
use std::io::{BufRead, BufReader, IsTerminal};
use std::path::Path;
use walkdir::WalkDir;
/// Busca texto en archivos con expresiones regulares
#[derive(Parser)]
#[command(
name = "crustaceo-grep",
version,
about = "grep en Rust con colores, recursividad y regex",
)]
struct Args {
/// Patrón de búsqueda (expresión regular)
patron: String,
/// Archivo(s) a buscar
archivos: Vec<String>,
/// Búsqueda recursiva en directorios
#[arg(short = 'r', long = "recursive")]
recursivo: bool,
/// Ignorar mayúsculas/minúsculas
#[arg(short = 'i', long = "ignore-case")]
ignorar_mayusculas: bool,
/// Solo mostrar el conteo de coincidencias por archivo
#[arg(short = 'c', long = "count")]
solo_conteo: bool,
/// Mostrar número de línea
#[arg(short = 'n', long = "line-number")]
num_linea: bool,
}
/// Recorre un directorio recursivamente y devuelve archivos regulares
fn archivos_recursivos(dir: &Path) -> Vec<String> {
WalkDir::new(dir)
.into_iter()
.filter_map(|e| match e {
Ok(entry) if entry.file_type().is_file() => {
Some(entry.path().to_string_lossy().to_string())
}
_ => None,
})
.collect()
}
/// Determina si el terminal soporta colores
fn color_activo() -> bool {
std::io::stdout().is_terminal()
}
/// Busca el patrón en un archivo y muestra resultados.
/// Devuelve true si al menos hubo una coincidencia.
fn buscar_en_archivo(
ruta: &str,
re: ®ex::Regex,
args: &Args,
) -> Result<bool> {
let archivo = File::open(ruta)
.with_context(|| format!("No se puede abrir '{}'", ruta))?;
let lector = BufReader::new(archivo);
let usar_color = color_activo();
let mut total_coincidencias = 0u64;
let mut hubo_match = false;
for (num_linea, linea) in lector.lines().enumerate() {
let linea = linea?;
let num_mostrar = num_linea + 1; // 1-indexed
// Buscar todas las coincidencias en esta línea
let matches: Vec<_> = re.find_iter(&linea).collect();
if !matches.is_empty() {
hubo_match = true;
total_coincidencias += matches.len() as u64;
// Modo conteo: no mostramos las líneas, solo acumulamos
if args.solo_conteo {
continue;
}
// Mostrar nombre de archivo si hay múltiples fuentes
if args.archivos.len() > 1 || args.recursivo {
print!("{}:", ruta);
}
// Número de línea
if args.num_linea {
print!("{}:", num_mostrar);
}
// Mostrar la línea con las coincidencias coloreadas
if usar_color {
mostrar_linea_coloreada(&linea, &matches);
} else {
println!("{}", linea);
}
}
}
// Modo conteo: mostrar total después de procesar
if args.solo_conteo && hubo_match {
if args.archivos.len() > 1 || args.recursivo {
print!("{}:", ruta);
}
if args.num_linea {
print!("{}:", total_coincidencias);
}
println!("{}", total_coincidencias);
}
Ok(hubo_match)
}
/// Muestra una línea con las coincidencias resaltadas en rojo y negrita
fn mostrar_linea_coloreada(linea: &str, matches: &[regex::Match]) {
let mut ultimo_fin = 0;
for m in matches {
// Parte antes de la coincidencia
let antes = &linea[ultimo_fin..m.start()];
print!("{}", antes);
// La coincidencia en rojo y negrita
let coincidencia = &linea[m.start()..m.end()];
print!("{}", coincidencia.red().bold());
ultimo_fin = m.end();
}
// Resto de la línea después de la última coincidencia
let resto = &linea[ultimo_fin..];
println!("{}", resto);
}
fn main() -> Result<()> {
let args = Args::parse();
// Construir la lista de archivos
let archivos: Vec<String> = if args.recursivo {
if args.archivos.is_empty() {
archivos_recursivos(Path::new("."))
} else {
let mut todos = Vec::new();
for ruta in &args.archivos {
let p = Path::new(ruta);
if p.is_dir() {
todos.extend(archivos_recursivos(p));
} else {
todos.push(ruta.clone());
}
}
todos
}
} else {
if args.archivos.is_empty() {
anyhow::bail!("Necesitas especificar al menos un archivo o usar --recursive");
}
args.archivos.clone()
};
if archivos.is_empty() {
eprintln!("No se encontraron archivos para buscar");
std::process::exit(1);
}
// Compilar la expresión regular (una vez, para todos los archivos)
let re = RegexBuilder::new(&args.patron)
.case_insensitive(args.ignorar_mayusculas)
.build()
.with_context(|| format!("Patrón regex inválido: '{}'", args.patron))?;
// Buscar en cada archivo
let mut hay_coincidencias = false;
for archivo in &archivos {
match buscar_en_archivo(archivo, &re, &args) {
Ok(true) => { hay_coincidencias = true; }
Ok(false) => { /* no hay match en este archivo, seguir */ }
Err(e) => {
eprintln!("Error procesando '{}': {}", archivo, e);
}
}
}
// Código de salida: 0 si hay match, 1 si no
if hay_coincidencias {
Ok(())
} else {
std::process::exit(1);
}
}
Este es el código completo. Vamos a desgranarlo pieza por pieza.
Cómo funciona la función buscar_en_archivo
Esta es la función más importante del programa. Procesa un archivo línea por línea con BufReader, y para cada línea:
- Busca todas las coincidencias con
re.find_iter(&linea). Es clave que esto devuelva unVec<regex::Match>— cada Match tiene.start()y.end()(posiciones en bytes) y.as_str()(el texto capturado). - Si hay coincidencias, incrementa el contador y, si no estamos en modo
--count, muestra la línea. - Para mostrar la línea coloreada, la función
mostrar_linea_coloreadarecorre los matches y va imprimiendo trozos: texto normal antes del match, texto en rojo+negrita para el match. - Si hay más de un archivo (o estamos en modo recursivo), añade el prefijo
nombre_archivo:. Si está activo--line-number, añadenumero:.
Cómo funciona el coloreado
La función mostrar_linea_coloreada itera sobre las coincidencias en orden. Usa un índice ultimo_fin para saber dónde se quedó. Para cada match, imprime:
[texto anterior (normal)] + [coincidencia (rojo+negrita)]
Y al final, imprime el resto de la línea después de la última coincidencia.
Esto es importante: las posiciones de regex::Match están en bytes, pero estamos usando slicing en strings UTF-8. Esto funciona porque regex garantiza que los matches están en límites de bytes válidos para UTF-8. Si el string no fuera UTF-8 válido, BufReader::lines() lo rechazaría antes, así que estamos seguros.
Cómo funciona el modo recursivo
Cuando --recursive está activo, archivos_recursivos() usa walkdir para escarbar en el directorio. Pero además, si el usuario pasa argumentos mixtos (un archivo y un directorio), el código en main() los trata individualmente:
for ruta in &args.archivos {
let p = Path::new(ruta);
if p.is_dir() {
todos.extend(archivos_recursivos(p));
} else {
todos.push(ruta.clone());
}
}
Cada argumento puede ser un archivo (se añade directamente) o un directorio (se expande recursivamente). Sin argumentos y con -r, busca desde el directorio actual.
Código de salida
El requisito final es: código de salida 0 si hay al menos una coincidencia, 1 si no hay ninguna. Esto es exactamente lo que hace grep.
En main():
if hay_coincidencias {
Ok(())
} else {
std::process::exit(1);
}
Ok(()) sale con 0. std::process::exit(1) sale con 1 inmediatamente.
Compilación y verificación
Compila el proyecto:
cargo build --release
Si no hay errores, el binario está en target/release/crustaceo-grep. Vamos a probarlo.
Prueba 1: búsqueda básica
# Crear un archivo de prueba
cat > /tmp/test-grep.txt << 'EOF'
Esto es una línea normal
ERROR: conexión fallida con el servidor
Esto también es normal
ERROR: timeout en la respuesta
Y esto otra línea normal
EOF
./target/release/crustaceo-grep "ERROR" /tmp/test-grep.txt
Deberías ver las dos líneas con «ERROR» resaltado en rojo y negrita.
Prueba 2: números de línea
./target/release/crustaceo-grep -n "ERROR" /tmp/test-grep.txt
# 2:ERROR: conexión fallida con el servidor
# 4:ERROR: timeout en la respuesta
Prueba 3: ignorar mayúsculas
./target/release/crustaceo-grep -i -n "error" /tmp/test-grep.txt
# 2:ERROR: conexión fallida con el servidor
# 4:ERROR: timeout en la respuesta
Prueba 4: conteo
./target/release/crustaceo-grep -c -i "error" /tmp/test-grep.txt
# 2
Prueba 5: búsqueda recursiva
# Crear estructura de prueba
mkdir -p /tmp/test-rec
cp /tmp/test-grep.txt /tmp/test-rec/log1.txt
cat > /tmp/test-rec/sub/log2.txt << 'EOF'
INFO: inicio del sistema
WARNING: disco al 80%
ERROR: no se puede montar /data
EOF
./target/release/crustaceo-grep -r -n "ERROR" /tmp/test-rec
# /tmp/test-rec/log1.txt:2:ERROR: conexión fallida con el servidor
# /tmp/test-rec/log1.txt:4:ERROR: timeout en la respuesta
# /tmp/test-rec/sub/log2.txt:3:ERROR: no se puede montar /data
Prueba 6: código de salida
./target/release/crustaceo-grep "ERROR" /tmp/test-grep.txt > /dev/null 2>&1
echo $?
# 0 — hay coincidencias
./target/release/crustaceo-grep "NOEXISTE" /tmp/test-grep.txt > /dev/null 2>&1
echo $?
# 1 — no hay coincidencias
Prueba 7: regex avanzada
# Patrón con regex: busca líneas con un número de 3 dígitos
cat > /tmp/test-regex.txt << 'EOF'
Código 200: OK
Código 404: Not Found
Código 500: Internal Server Error
EOF
./target/release/crustaceo-grep -n "[45]0[04]" /tmp/test-regex.txt
# 2:Código 404: Not Found
# 3:Código 500: Internal Server Error
# Alternativamente, buscar líneas con una IP
cat > /tmp/test-ip.txt << 'EOF'
192.168.1.1 - - [24/Jun/2026] "GET /" 200
10.0.0.5 - - [24/Jun/2026] "POST /api" 500
EOF
./target/release/crustaceo-grep -n '192\.168\..* 200' /tmp/test-ip.txt
# 1:192.168.1.1 - - [24/Jun/2026] "GET /" 200
Prueba 8: sin colores al redirigir
./target/release/crustaceo-grep "ERROR" /tmp/test-grep.txt > /tmp/salida.txt
cat /tmp/salida.txt
# Sin códigos ANSI — texto plano y limpio
Resumen
| Concepto | Código clave | Equivalente Bash |
|---|---|---|
| Definir argumentos CLI | #[derive(Parser)] + struct | getopts o manual $1, $2 |
| Flag booleano | #[arg(short, long)] | -r en getopts |
| Búsqueda recursiva | WalkDir::new(dir) | find . -type f |
| Regex | RegexBuilder::new(p).build() | grep -E |
| Ignorar mayúsculas | .case_insensitive(true) | grep -i |
| Coincidencias en línea | re.find_iter(&linea) | No hay equivalente directo |
| Coloreado | texto .red().bold() | grep --color=always |
| Detectar terminal | stdout.is_terminal() | grep --color=auto |
| Código de salida | exit(1) si no hay match | echo $? |
En este capítulo has construido crustaceo-grep, un clon funcional de grep en Rust que:
- Toma un patrón regex y uno o varios archivos
- Busca recursivamente con
-rusandowalkdir - Muestra coincidencias coloreadas con
colored(solo en terminal) - Ofrece flags
-i(mayúsculas),-c(conteo),-n(líneas),-r(recursivo) - Usa
clap 4con derive para argumentos CLI profesionales - Devuelve código de salida 0 si hay match, 1 si no
Y lo has hecho con menos de 200 líneas de Rust, funcionales, compilables y listas para usar.
Mejoras que puedes implementar
El proyecto es funcional, pero puedes extenderlo fácilmente:
- Soporte para stdin — si no se pasan archivos, leer de la entrada estándar (como hace grep)
- Excluir patrones con
--excludeo--exclude-dir(como--exclude-dir=.giten grep) - Contexto — flag
-C NUMpara mostrar líneas antes y después de la coincidencia - Solo nombres de archivo — flag
-lpara listar solo los archivos con coincidencias - Soporte para múltiples patrones — flag
-epara pasar varios patrones (OR lógico) - Salida con paginación — integrar con
less -Rautomáticamente - Archivos binarios — detectar si un archivo es binario y saltarlo
- Follow simlinks —
walkdirtiene.follow_links(true)
En el próximo capítulo veremos cómo leer y escribir archivos de configuración en formatos estándar (TOML, YAML, JSON), cómo persistir datos y cómo crear herramientas configurables sin hardcodear rutas.
Verificación
Script de verificación completo para asegurarte de que todo funciona:
#!/bin/bash
# test-crustaceo-grep.sh
set -e
echo "🔍 Prueba 1: Compilación"
cd ~/crustaceo-grep
cargo build --release 2>&1 | tail -1
echo ""
echo "🔍 Prueba 2: Ayuda"
./target/release/crustaceo-grep --help | head -5
echo ""
echo "🔍 Prueba 3: Búsqueda básica"
cat > /tmp/grep-test.txt << 'EOF'
línea uno
ERROR: algo falló
línea tres
ERROR: otro fallo
EOF
./target/release/crustaceo-grep "ERROR" /tmp/grep-test.txt | wc -l
# Debe mostrar 2
echo ""
echo "🔍 Prueba 4: Números de línea"
./target/release/crustaceo-grep -n "ERROR" /tmp/grep-test.txt
echo ""
echo "🔍 Prueba 5: Ignorar mayúsculas"
./target/release/crustaceo-grep -i -n "error" /tmp/grep-test.txt | wc -l
# Debe mostrar 2
echo ""
echo "🔍 Prueba 6: Conteo"
./target/release/crustaceo-grep -c "ERROR" /tmp/grep-test.txt
# Debe mostrar 2
echo ""
echo "🔍 Prueba 7: Código de salida (con match)"
./target/release/crustaceo-grep "ERROR" /tmp/grep-test.txt > /dev/null
echo "Código: $?" # Debe ser 0
echo ""
echo "🔍 Prueba 8: Código de salida (sin match)"
./target/release/crustaceo-grep "NOEXISTE" /tmp/grep-test.txt > /dev/null 2>&1
echo "Código: $?" # Debe ser 1
echo ""
echo "🔍 Prueba 9: Búsqueda recursiva"
mkdir -p /tmp/grep-rec/sub
cp /tmp/grep-test.txt /tmp/grep-rec/
echo "WARNING: algo raro" > /tmp/grep-rec/sub/otro.log
./target/release/crustaceo-grep -r -n "ERROR" /tmp/grep-rec
# Debe mostrar 2 líneas con la ruta completa
echo ""
echo "🔍 Prueba 10: Regex avanzada"
echo "código 200: ok" > /tmp/grep-regex.txt
echo "código 404: error" >> /tmp/grep-regex.txt
./target/release/crustaceo-grep -n "^.*4[0-9][0-9].*error$" /tmp/grep-regex.txt
echo ""
echo "✅ Todas las pruebas completadas"
Ejecuta:
chmod +x test-crustaceo-grep.sh
./test-crustaceo-grep.sh
Cada prueba debe mostrar la salida esperada. Si alguna falla, revisa el código correspondiente.
Más información,
- Tutorial Scripts en Bash en atareao.es — el punto de partida: en Bash
grepes un comando externo, en Rust lo construyes desde cero con crates modulares - Tutorial Sed y Awk en atareao.es — procesamiento de texto en Linux, donde grep encaja como una pieza más del ecosistema
- Tutorial de herramientas Linux en atareao.es — gestión de herramientas, binarios y automatización en Linux
- Crate clap en crates.io — la crate de referencia para CLI argumentos en Rust
- Documentación de clap 4.x — API completa con ejemplos de derive y builder
- Crate regex en crates.io — expresiones regulares para Rust, más rápidas que las de Perl
- Documentación de regex — Rust — API completa, sintaxis soportada y ejemplos
- Crate walkdir en crates.io — iterador recursivo de directorios
- Documentación de walkdir — control de profundidad, simlinks, errores
- Crate colored en crates.io — coloración de terminal simple y efectiva
- Crate anyhow en crates.io — manejo de errores simplificado para aplicaciones
- std::io::IsTerminal — documentación oficial — detectar si la salida es un terminal
- BufReader — documentación oficial — lectura eficiente línea por línea
- Rust by Example — Expresiones regulares
- Rust by Example — clap
- clap derive tutorial oficial — la guía paso a paso de la macro derive
- GNU grep manual — para comparar flags y comportamiento