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, /// 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 { 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 { 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 = 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); } }