Ultima attività 6 hours ago

atareao's Avatar atareao ha revisionato questo gist 6 hours ago. Vai alla revisione

1 file changed, 200 insertions

crustaceo-grep.rs(file creato)

@@ -0,0 +1,200 @@
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: &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
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 + }
Più nuovi Più vecchi