crustaceo-grep.rs
· 5.6 KiB · Rust
Surowy
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);
}
}
| 1 | use anyhow::{Context, Result}; |
| 2 | use clap::Parser; |
| 3 | use colored::*; |
| 4 | use regex::RegexBuilder; |
| 5 | use std::fs::File; |
| 6 | use std::io::{BufRead, BufReader, IsTerminal}; |
| 7 | use std::path::Path; |
| 8 | use walkdir::WalkDir; |
| 9 | |
| 10 | /// Busca texto en archivos con expresiones regulares |
| 11 | #[derive(Parser)] |
| 12 | #[command( |
| 13 | name = "crustaceo-grep", |
| 14 | version, |
| 15 | about = "grep en Rust con colores, recursividad y regex", |
| 16 | )] |
| 17 | struct Args { |
| 18 | /// Patrón de búsqueda (expresión regular) |
| 19 | patron: String, |
| 20 | |
| 21 | /// Archivo(s) a buscar |
| 22 | archivos: Vec<String>, |
| 23 | |
| 24 | /// Búsqueda recursiva en directorios |
| 25 | #[arg(short = 'r', long = "recursive")] |
| 26 | recursivo: bool, |
| 27 | |
| 28 | /// Ignorar mayúsculas/minúsculas |
| 29 | #[arg(short = 'i', long = "ignore-case")] |
| 30 | ignorar_mayusculas: bool, |
| 31 | |
| 32 | /// Solo mostrar el conteo de coincidencias por archivo |
| 33 | #[arg(short = 'c', long = "count")] |
| 34 | solo_conteo: bool, |
| 35 | |
| 36 | /// Mostrar número de línea |
| 37 | #[arg(short = 'n', long = "line-number")] |
| 38 | num_linea: bool, |
| 39 | } |
| 40 | |
| 41 | /// Recorre un directorio recursivamente y devuelve archivos regulares |
| 42 | fn archivos_recursivos(dir: &Path) -> Vec<String> { |
| 43 | WalkDir::new(dir) |
| 44 | .into_iter() |
| 45 | .filter_map(|e| match e { |
| 46 | Ok(entry) if entry.file_type().is_file() => { |
| 47 | Some(entry.path().to_string_lossy().to_string()) |
| 48 | } |
| 49 | _ => None, |
| 50 | }) |
| 51 | .collect() |
| 52 | } |
| 53 | |
| 54 | /// Determina si el terminal soporta colores |
| 55 | fn color_activo() -> bool { |
| 56 | std::io::stdout().is_terminal() |
| 57 | } |
| 58 | |
| 59 | /// Busca el patrón en un archivo y muestra resultados. |
| 60 | /// Devuelve true si al menos hubo una coincidencia. |
| 61 | fn buscar_en_archivo( |
| 62 | ruta: &str, |
| 63 | re: ®ex::Regex, |
| 64 | args: &Args, |
| 65 | ) -> Result<bool> { |
| 66 | let archivo = File::open(ruta) |
| 67 | .with_context(|| format!("No se puede abrir '{}'", ruta))?; |
| 68 | let lector = BufReader::new(archivo); |
| 69 | let usar_color = color_activo(); |
| 70 | |
| 71 | let mut total_coincidencias = 0u64; |
| 72 | let mut hubo_match = false; |
| 73 | |
| 74 | for (num_linea, linea) in lector.lines().enumerate() { |
| 75 | let linea = linea?; |
| 76 | let num_mostrar = num_linea + 1; // 1-indexed |
| 77 | |
| 78 | // Buscar todas las coincidencias en esta línea |
| 79 | let matches: Vec<_> = re.find_iter(&linea).collect(); |
| 80 | |
| 81 | if !matches.is_empty() { |
| 82 | hubo_match = true; |
| 83 | total_coincidencias += matches.len() as u64; |
| 84 | |
| 85 | // Modo conteo: no mostramos las líneas, solo acumulamos |
| 86 | if args.solo_conteo { |
| 87 | continue; |
| 88 | } |
| 89 | |
| 90 | // Mostrar nombre de archivo si hay múltiples fuentes |
| 91 | if args.archivos.len() > 1 || args.recursivo { |
| 92 | print!("{}:", ruta); |
| 93 | } |
| 94 | |
| 95 | // Número de línea |
| 96 | if args.num_linea { |
| 97 | print!("{}:", num_mostrar); |
| 98 | } |
| 99 | |
| 100 | // Mostrar la línea con las coincidencias coloreadas |
| 101 | if usar_color { |
| 102 | mostrar_linea_coloreada(&linea, &matches); |
| 103 | } else { |
| 104 | println!("{}", linea); |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | // Modo conteo: mostrar total después de procesar |
| 110 | if args.solo_conteo && hubo_match { |
| 111 | if args.archivos.len() > 1 || args.recursivo { |
| 112 | print!("{}:", ruta); |
| 113 | } |
| 114 | if args.num_linea { |
| 115 | print!("{}:", total_coincidencias); |
| 116 | } |
| 117 | println!("{}", total_coincidencias); |
| 118 | } |
| 119 | |
| 120 | Ok(hubo_match) |
| 121 | } |
| 122 | |
| 123 | /// Muestra una línea con las coincidencias resaltadas en rojo y negrita |
| 124 | fn mostrar_linea_coloreada(linea: &str, matches: &[regex::Match]) { |
| 125 | let mut ultimo_fin = 0; |
| 126 | |
| 127 | for m in matches { |
| 128 | // Parte antes de la coincidencia |
| 129 | let antes = &linea[ultimo_fin..m.start()]; |
| 130 | print!("{}", antes); |
| 131 | |
| 132 | // La coincidencia en rojo y negrita |
| 133 | let coincidencia = &linea[m.start()..m.end()]; |
| 134 | print!("{}", coincidencia.red().bold()); |
| 135 | |
| 136 | ultimo_fin = m.end(); |
| 137 | } |
| 138 | |
| 139 | // Resto de la línea después de la última coincidencia |
| 140 | let resto = &linea[ultimo_fin..]; |
| 141 | println!("{}", resto); |
| 142 | } |
| 143 | |
| 144 | fn main() -> Result<()> { |
| 145 | let args = Args::parse(); |
| 146 | |
| 147 | // Construir la lista de archivos |
| 148 | let archivos: Vec<String> = if args.recursivo { |
| 149 | if args.archivos.is_empty() { |
| 150 | archivos_recursivos(Path::new(".")) |
| 151 | } else { |
| 152 | let mut todos = Vec::new(); |
| 153 | for ruta in &args.archivos { |
| 154 | let p = Path::new(ruta); |
| 155 | if p.is_dir() { |
| 156 | todos.extend(archivos_recursivos(p)); |
| 157 | } else { |
| 158 | todos.push(ruta.clone()); |
| 159 | } |
| 160 | } |
| 161 | todos |
| 162 | } |
| 163 | } else { |
| 164 | if args.archivos.is_empty() { |
| 165 | anyhow::bail!("Necesitas especificar al menos un archivo o usar --recursive"); |
| 166 | } |
| 167 | args.archivos.clone() |
| 168 | }; |
| 169 | |
| 170 | if archivos.is_empty() { |
| 171 | eprintln!("No se encontraron archivos para buscar"); |
| 172 | std::process::exit(1); |
| 173 | } |
| 174 | |
| 175 | // Compilar la expresión regular (una vez, para todos los archivos) |
| 176 | let re = RegexBuilder::new(&args.patron) |
| 177 | .case_insensitive(args.ignorar_mayusculas) |
| 178 | .build() |
| 179 | .with_context(|| format!("Patrón regex inválido: '{}'", args.patron))?; |
| 180 | |
| 181 | // Buscar en cada archivo |
| 182 | let mut hay_coincidencias = false; |
| 183 | |
| 184 | for archivo in &archivos { |
| 185 | match buscar_en_archivo(archivo, &re, &args) { |
| 186 | Ok(true) => { hay_coincidencias = true; } |
| 187 | Ok(false) => { /* no hay match en este archivo, seguir */ } |
| 188 | Err(e) => { |
| 189 | eprintln!("Error procesando '{}': {}", archivo, e); |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | // Código de salida: 0 si hay match, 1 si no |
| 195 | if hay_coincidencias { |
| 196 | Ok(()) |
| 197 | } else { |
| 198 | std::process::exit(1); |
| 199 | } |
| 200 | } |