4

Construye tu propio grep en Rust para Linuxeros

Vistas: 1
Construye tu propio grep en Rust para Linuxeros

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 derive habilitada, defines los argumentos como structs de Rust, y clap genera el parseo, la validación y el mensaje --help automá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-automata de 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: &regex::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:

  1. Busca todas las coincidencias con re.find_iter(&linea). Es clave que esto devuelva un Vec<regex::Match> — cada Match tiene .start() y .end() (posiciones en bytes) y .as_str() (el texto capturado).
  2. Si hay coincidencias, incrementa el contador y, si no estamos en modo --count, muestra la línea.
  3. Para mostrar la línea coloreada, la función mostrar_linea_coloreada recorre los matches y va imprimiendo trozos: texto normal antes del match, texto en rojo+negrita para el match.
  4. Si hay más de un archivo (o estamos en modo recursivo), añade el prefijo nombre_archivo:. Si está activo --line-number, añade numero:.

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

ConceptoCódigo claveEquivalente Bash
Definir argumentos CLI#[derive(Parser)] + structgetopts o manual $1, $2
Flag booleano#[arg(short, long)]-r en getopts
Búsqueda recursivaWalkDir::new(dir)find . -type f
RegexRegexBuilder::new(p).build()grep -E
Ignorar mayúsculas.case_insensitive(true)grep -i
Coincidencias en líneare.find_iter(&linea)No hay equivalente directo
Coloreadotexto .red().bold()grep --color=always
Detectar terminalstdout.is_terminal()grep --color=auto
Código de salidaexit(1) si no hay matchecho $?

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 -r usando walkdir
  • Muestra coincidencias coloreadas con colored (solo en terminal)
  • Ofrece flags -i (mayúsculas), -c (conteo), -n (líneas), -r (recursivo)
  • Usa clap 4 con 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 --exclude o --exclude-dir (como --exclude-dir=.git en grep)
  • Contexto — flag -C NUM para mostrar líneas antes y después de la coincidencia
  • Solo nombres de archivo — flag -l para listar solo los archivos con coincidencias
  • Soporte para múltiples patrones — flag -e para pasar varios patrones (OR lógico)
  • Salida con paginación — integrar con less -R automáticamente
  • Archivos binarios — detectar si un archivo es binario y saltarlo
  • Follow simlinkswalkdir tiene .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,

Deja una respuesta