Módulo 2: Trabajar con archivos y texto

6. Buscar dentro de archivos con grep

Descripción

En la lección anterior aprendiste a encontrar archivos por su nombre, tipo, tamaño o fecha con find. Pero find solo mira las etiquetas de las cajas: nunca abre ninguna. Esta lección te enseña a hacer lo otro: abrir el contenido de uno o de miles de archivos a la vez y encontrar la línea exacta que buscas, sin abrir un editor y sin mirar archivo por archivo.

Esto no es un ejercicio académico. Antes de renombrar una función en un proyecto de doscientos archivos necesitas saber en cuáles se usa. Antes de hacer un commit quieres verificar que no dejaste una contraseña o una llave de API escrita a mano en el código. Cuando un servidor falla a las tres de la mañana, revisas un archivo de log de dos millones de líneas buscando la palabra ERROR y el minuto exacto en que empezó. En los tres casos la respuesta es la misma herramienta: grep.

Conexión con el módulo: find te dice dónde están los archivos; grep te dice qué hay adentro. Juntos resuelven el problema completo de orientarte en un proyecto que no conoces —justo lo que vas a practicar en el proyecto final de este módulo.

De localizar archivos a leer su contenido

Piensa en find como el índice de una biblioteca: te dice en qué estante está cada libro, por su título, su tamaño o la fecha en que llegó, pero nunca lo abre. grep es el bibliotecario que sí abre cada libro, lee cada línea de cada página, y te dice exactamente en cuál aparece la frase que buscas. La diferencia importa porque son preguntas distintas: "¿dónde está el archivo config.py?" la responde find; "¿en qué archivo y en qué línea aparece la palabra PAYMENT_API_KEY?" la responde grep.

El nombre viene de global regular expression print: recorre línea por línea un texto, evalúa cada línea contra un patrón, e imprime las que coinciden. La forma más simple es:

grep patrón archivo

Ejemplo trabajado

Imagina que tienes un archivo de log de tu aplicación, access.log, con este contenido:

INFO  2026-07-18 09:12:03 User 42 logged in
ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
WARN  2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
INFO  2026-07-18 09:20:44 User 17 logged in
ERROR 2026-07-18 09:22:19 Database connection refused

Quieres ver solo las líneas donde algo falló:

grep ERROR access.log

Qué esperar:

ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:22:19 Database connection refused

grep leyó las seis líneas, comparó cada una contra el patrón ERROR e imprimió únicamente las tres que lo contienen. Nada de esto abrió un editor ni requirió que supieras en qué parte del archivo estaba el error.

Ahora el salto de calidad real: en vez de un archivo, tienes un proyecto entero. Imagina esta estructura, web-store/:

web-store/
├── app.py
├── config.py
├── README.md
├── .git/
│   └── (metadata interna de git, no es código tuyo)
├── node_modules/
│   └── (miles de archivos de dependencias)
└── tests/
    └── test_orders.py

Donde config.py tiene:

PAYMENT_API_KEY = "sk_live_FAKEEXAMPLE1234567890"

app.py tiene:

import requests
from config import PAYMENT_API_KEY

def charge_customer(order_id, amount):
    headers = {"Authorization": PAYMENT_API_KEY}
    return requests.post(
        "https://api.payments.example/charge",
        headers=headers,
        json={"order_id": order_id, "amount": amount},
    )

Y tests/test_orders.py tiene:

# Verifica que PAYMENT_API_KEY esté cargada antes de correr las pruebas de integración
from config import PAYMENT_API_KEY

Quieres saber en qué archivos aparece PAYMENT_API_KEY, sin abrir cada uno a mano. Con -r (recursivo), grep entra a cada subdirectorio por ti:

grep -rn PAYMENT_API_KEY web-store/

Qué esperar:

web-store/app.py:2:from config import PAYMENT_API_KEY
web-store/app.py:5:    headers = {"Authorization": PAYMENT_API_KEY}
web-store/config.py:1:PAYMENT_API_KEY = "sk_live_FAKEEXAMPLE1234567890"
web-store/tests/test_orders.py:1:# Verifica que PAYMENT_API_KEY esté cargada antes de correr las pruebas de integración
web-store/tests/test_orders.py:2:from config import PAYMENT_API_KEY

Con un solo comando ubicaste las cinco líneas exactas, en tres archivos distintos, dentro de un árbol de directorios que ni siquiera tuviste que recorrer a mano. Ese es el salto que da -r: pasas de "buscar en un archivo" a "buscar en tu proyecto completo".

Las opciones que usarás todos los días

Estas seis cubren la enorme mayoría de tus búsquedas reales. Todas se combinan entre sí y con -r.

-i — ignorar mayúsculas y minúsculas. Por defecto grep distingue entre mayúsculas y minúsculas. access.log tiene dos líneas con la palabra User (con mayúscula):

grep user access.log

Qué esperar: nada. Cero líneas, porque user (minúscula) no es igual a User.

grep -i user access.log

Qué esperar:

INFO  2026-07-18 09:12:03 User 42 logged in
INFO  2026-07-18 09:20:44 User 17 logged in

-n — mostrar el número de línea. Ya lo usaste arriba (-rn). Sin él, grep te dice qué línea coincide pero no dónde está dentro del archivo; con él, puedes ir directo a esa línea en tu editor.

-w — solo palabra completa. Por defecto grep busca la cadena en cualquier posición, incluso a mitad de otra palabra. Busca log en access.log:

grep log access.log

Qué esperar:

INFO  2026-07-18 09:12:03 User 42 logged in
INFO  2026-07-18 09:20:44 User 17 logged in

Ninguna de esas líneas tiene la palabra suelta "log": lo que coincidió fue el fragmento log dentro de logged. Con -w exiges que el patrón sea una palabra completa, delimitada por espacios, puntuación o el borde de la línea:

grep -w log access.log

Qué esperar: nada. access.log no tiene ninguna línea con la palabra exacta "log".

-c — contar coincidencias en vez de mostrarlas.

grep -c ERROR access.log

Qué esperar:

3

-l — mostrar solo el nombre de los archivos que tienen coincidencias, no las líneas. Es la opción que quieres cuando la pregunta es "¿en cuáles archivos aparece esto?" y no te interesa todavía el detalle:

grep -rl PAYMENT_API_KEY web-store/

Qué esperar:

web-store/app.py
web-store/config.py
web-store/tests/test_orders.py

-v — invertir la búsqueda, mostrar las líneas que no coinciden. Útil para quitar ruido: quieres ver todo lo que no sea tráfico rutinario.

grep -v INFO access.log

Qué esperar:

ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
WARN  2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:22:19 Database connection refused

Ver el contexto de una coincidencia

Una línea suelta a veces no te dice nada; necesitas ver qué pasó justo antes o justo después. Para eso están -A (after, líneas después), -B (before, líneas antes) y -C (context, ambos lados), todas seguidas de cuántas líneas quieres.

¿Qué pasó justo después de la advertencia (WARN)?

grep -A 1 WARN access.log

Qué esperar:

WARN  2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188

¿Qué pasó justo antes de que la base de datos rechazara la conexión?

grep -B 1 "Database connection refused" access.log

Qué esperar:

INFO  2026-07-18 09:20:44 User 17 logged in
ERROR 2026-07-18 09:22:19 Database connection refused

Y con -C obtienes ambos lados en un solo paso:

grep -C 1 WARN access.log

Qué esperar:

ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
WARN  2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188

Cuando hay varias coincidencias separadas y sus bloques de contexto no se tocan, grep los separa con una línea -- para que sepas que no son consecutivos en el archivo original.

Excluir directorios ruidosos con --exclude-dir

Volvamos a web-store/. El directorio .git/ guarda la historia completa del proyecto en un formato binario comprimido, y node_modules/ puede tener decenas de miles de archivos de dependencias que no escribiste tú. grep -r los recorre igual, uno por uno, aunque nunca vaya a encontrar nada útil ahí: eso vuelve la búsqueda visiblemente más lenta y agrega ruido si alguna coincidencia casual aparece en un archivo de una dependencia.

grep -rn --exclude-dir=node_modules --exclude-dir=.git PAYMENT_API_KEY web-store/

Qué esperar: el mismo resultado que obtuviste antes con -rn —las cinco líneas en app.py, config.py y test_orders.py— pero en una fracción del tiempo, porque grep nunca entra a esos dos directorios. Puedes repetir --exclude-dir tantas veces como directorios quieras excluir.

Expresiones regulares básicas

Hasta ahora buscaste texto literal. La verdadera potencia de grep aparece cuando el patrón describe una forma, no un texto exacto. Estos son los símbolos que vas a usar a diario:

SímboloSignificadoEjemploQué encuentra
.cualquier carácter (uno solo)order 11.8order 1188 (el . cubre el segundo 8)
*cero o más repeticiones del carácter anteriorcolou*rcolor y colour
+una o más repeticiones (necesita -E)colou+rcolour, pero no color
^inicio de línea^ERRORlíneas que empiezan con ERROR
$fin de líneain$líneas que terminan en in
[...]una clase de caracteres^[EW]líneas que empiezan con E o W
|alternancia — "esto o aquello" (necesita -E)WARN|refusedlíneas con WARN o con refused

Prueba las anclas sobre access.log:

grep '^ERROR' access.log

Qué esperar: las tres líneas ERROR (todas empiezan por ahí). Y para el otro extremo:

grep 'in$' access.log

Qué esperar: las dos líneas que terminan en "logged in".

Las clases de caracteres no necesitan nada especial:

grep '^[EW]' access.log

Qué esperar: las líneas ERROR y WARN, sin las INFO.

Para + y la alternancia (|) necesitas activar la sintaxis extendida con -E, porque por defecto grep usa expresiones regulares básicas (BRE), donde esos dos símbolos no tienen ese significado especial:

grep -E 'WARN|refused' access.log

Qué esperar:

WARN  2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:22:19 Database connection refused

Cuidado con la precedencia de |: separa el patrón completo a cada lado, no solo la palabra siguiente. ^ERROR|WARN no significa "la línea empieza con ERROR o con WARN": significa "la línea empieza con ERROR, o contiene WARN en cualquier posición". Si quieres anclar ambas alternativas, tienes que repetir el ^ en cada una: ^ERROR|^WARN.

Y el clásico colou*r / colou+r de la tabla no es un capricho: es el ejemplo de libro para manejar variantes de ortografía (color/colour, behavior/behaviour) sin escribir dos búsquedas.

Nota al margen: vas a ver referencias a egrep y fgrep en tutoriales viejos. Son atajos históricos equivalentes a grep -E y grep -F (búsqueda literal); GNU los marca como obsoletos y recomienda usar los flags explícitos.

Por qué el patrón siempre va entre comillas simples

El patrón de grep usa muchos de los mismos caracteres que tu shell interpreta antes de ejecutar cualquier comando: *, $, [, ], ?. Si escribes el patrón sin comillas, el shell puede expandirlo antes de que grep lo vea, y grep termina recibiendo algo distinto de lo que escribiste.

Ejemplo real: quieres buscar líneas que contengan un asterisco literal, o simplemente escribes un patrón con * sin pensarlo dos veces:

grep *.log access.log

Si tu directorio actual tiene algún archivo que termina en .log, el shell expande *.log a esos nombres de archivo antes de que grep arranque, y terminas ejecutando algo como grep access.log access.log —buscando el contenido de un archivo dentro de otro, no lo que querías. Si usas zsh (el shell por defecto en macOS) y ningún archivo coincide con el comodín, el error es todavía más directo: zsh: no matches found: *.log, y el comando ni siquiera llega a correr.

Las comillas simples ('patrón') evitan todo esto: nada dentro de ellas se expande, ni variables ($HOME), ni comodines, ni comandos. Es la opción segura por defecto. Las comillas dobles ("patrón") siguen expandiendo $variables, así que solo sirven si de verdad quieres que una variable de shell entre al patrón.

Honestidad: ripgrep existe y es mejor herramienta

ripgrep (el comando rg) es notablemente más rápido que grep en proyectos grandes, respeta .gitignore automáticamente —así que nunca tienes que acordarte de excluir node_modules/ o .git/— y por defecto ignora archivos binarios. Si trabajas todo el día en un mismo proyecto de código, vale la pena instalarlo.

Pero grep está en todas las máquinas Linux y en macOS sin instalar nada. Cuando te conectas por SSH a un servidor que administra otra persona, o auditas un contenedor recién creado, grep siempre está ahí. Por eso esta guía te enseña grep primero: es la herramienta que nunca falla, aunque no sea la más rápida.

Errores comunes

1. Creer que grep busca "palabras" por defecto (error conceptual). Qué pasa: buscas log esperando encontrar solo la palabra suelta "log", pero grep también te devuelve líneas con "logged", "logging" o "catalog", porque por defecto busca la cadena en cualquier posición, sea parte de otra palabra o no. Por qué: grep compara subcadenas, no tokens; no sabe qué es una "palabra" a menos que se lo pidas. Cómo detectarlo: tus resultados incluyen líneas que claramente no hablan de lo que buscabas. Cómo corregirlo: agrega -w para exigir coincidencia de palabra completa.

2. Escribir el patrón sin comillas simples (error conceptual sobre el orden de expansión). Qué pasa: usas un patrón con *, $ o [...] sin comillas, y grep se comporta de forma inesperada: busca en el archivo equivocado, dice "No such file or directory", o en zsh directamente aborta con "no matches found". Por qué: el shell expande comodines y variables antes de pasarle el argumento a grep; tu patrón nunca llega intacto. Cómo detectarlo: el comportamiento cambia según qué archivos existan en tu directorio actual, cosa que no debería importarle a una búsqueda de texto. Cómo corregirlo: encierra siempre el patrón en comillas simples.

3. Correr grep -r en la raíz de un repositorio sin excluir .git/ ni node_modules/. Qué pasa: la búsqueda tarda mucho más de lo esperado, y a veces grep imprime líneas como binary file web-store/.git/objects/... matches en vez de una línea de código legible. Por qué: .git/ guarda objetos comprimidos que grep interpreta como binarios, y node_modules/ puede tener decenas de miles de archivos irrelevantes. Cómo detectarlo: la búsqueda se siente lenta para un patrón simple, o aparecen menciones a "binary file" en la salida. Cómo corregirlo: agrega --exclude-dir=.git --exclude-dir=node_modules, o instala ripgrep, que excluye estos directorios por defecto.

Ejercicios

1. Cuenta cuántas líneas de access.log mencionan ERROR, sin listar las líneas mismas.

Ver solución
grep -c ERROR access.log

Resultado: 3. Por qué funciona: -c le dice a grep que reemplace la salida normal (las líneas que coinciden) por un solo número: cuántas coincidieron.

2. Sin cambiar el archivo, predice y luego verifica: ¿qué imprime grep user access.log? ¿Y grep -i user access.log?

Ver solución

grep user access.log no imprime nada, porque el archivo tiene "User" con mayúscula inicial y grep distingue mayúsculas de minúsculas por defecto. grep -i user access.log imprime las dos líneas con "User". Por qué funciona: -i le dice a grep que ignore la diferencia entre mayúsculas y minúsculas al comparar cada línea contra el patrón.

3. En web-store/, sin entrar a node_modules/ ni a .git/, encuentra en qué archivos aparece PAYMENT_API_KEY, mostrando solo los nombres de archivo (no las líneas).

Ver solución
grep -rl --exclude-dir=node_modules --exclude-dir=.git PAYMENT_API_KEY web-store/

Resultado: web-store/app.py, web-store/config.py y web-store/tests/test_orders.py. Por qué funciona: -r recorre subdirectorios, -l corta la salida a solo el nombre del archivo en vez de las líneas completas, y cada --exclude-dir evita que grep entre a un directorio específico.

4. Escribe un patrón con grep -E que encuentre las líneas de access.log que empiezan con ERROR o empiezan con WARN —no que solo las contengan en cualquier posición.

Ver solución
grep -E '^ERROR|^WARN' access.log

Por qué funciona: el operador | separa el patrón completo a cada lado, no solo la palabra siguiente. Si escribieras ^ERROR|WARN, el ancla ^ aplicaría solo al lado izquierdo, y WARN se buscaría en cualquier posición de la línea, no solo al inicio. Repetir el ^ en cada alternativa es lo que garantiza que ambas queden ancladas.

Resumen y siguiente paso

Con find y grep juntos ya puedes ubicar cualquier archivo de tu máquina y leer su contenido buscando exactamente lo que necesitas, sin abrir un editor y sin mirar carpeta por carpeta. Eso es exactamente lo que vas a practicar en el proyecto final de este módulo, auditando un proyecto que no conoces: find para entender qué hay, grep para entender qué dice.

Más adelante, en el módulo de composición, vas a conectar la salida de grep directamente con otros comandos usando pipes —pero por ahora concéntrate en dominarlo por sí solo.

Todo lo que hiciste en esta lección fue buscar y leer: nunca cambiaste una sola línea de ningún archivo. Ese es justamente el hueco que cierra la siguiente lección —vas a poder editar archivos directamente desde la terminal, con nano para lo cotidiano y lo mínimo indispensable de vim para el día en que se abra sin que lo hayas pedido.

Antes de avanzar deberías poder: buscar un patrón de texto en un archivo y en un proyecto completo con -r; ajustar esa búsqueda con -i, -n, -w, -c, -l y -v; ver el contexto alrededor de una coincidencia con -A, -B y -C; excluir directorios ruidosos con --exclude-dir; y escribir un patrón básico con anclas, clases de caracteres y alternancia, siempre entre comillas simples.

Recursos