Last active 6 hours ago

Revision 0667d06eb422fb4020ebc808ccb972a1d69dfafb

crustaceo-grep.rs Raw
1use anyhow::{Context, Result};
2use clap::Parser;
3use colored::*;
4use regex::RegexBuilder;
5use std::fs::File;
6use std::io::{BufRead, BufReader, IsTerminal};
7use std::path::Path;
8use 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)]
17struct 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
42fn 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
55fn 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.
61fn buscar_en_archivo(
62 ruta: &str,
63 re: &regex::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
124fn 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
144fn 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}