Recuerdas que una función calcula el total de una compra, pero no en qué paquete vive. Buscas su nombre y aparecen la implementación, los tests, una copia compilada y varias dependencias. Después encuentras el archivo correcto y todavía tienes que copiar su ruta para abrirlo.
En ese recorrido hay tres tareas diferentes: buscar dentro del código, encontrar archivos y elegir un resultado. ripgrep, fd y fzf permiten resolverlas por separado y combinarlas mediante las tuberías de la terminal.
No necesitas reemplazar todos tus comandos ni abandonar el buscador del editor. Vamos a construir un repositorio pequeño en TypeScript para comprobar dónde ayudan estas herramientas y dónde siguen siendo necesarios grep, find o las funciones del IDE.
Qué aporta cada herramienta
| Herramienta | Pregunta que resuelve | Ejemplo |
|---|---|---|
ripgrep, ejecutable rg | ¿Qué archivos contienen este texto? | rg -n 'calculateTotal' src |
fd | ¿Qué archivos o directorios tienen este nombre? | fd -t f -e ts calculateTotal |
fzf | ¿Cuál de estos resultados quiero seleccionar? | fd -t f -e ts | fzf |
Una tubería, |, conecta la salida de un programa con la entrada del siguiente. En el último ejemplo, fd produce las rutas y fzf permite elegir una. No se está buscando el contenido de los archivos.
ripgrep frente a grep
ripgrep busca patrones de texto. Al recibir un directorio, lo recorre recursivamente; si no indicas una ruta y no recibe datos por una tubería, busca desde el directorio actual.
Estas dos órdenes sirven para investigar una función dentro de src:
grep -R -n 'calculateTotal' src
rg -n 'calculateTotal' src-n pide los números de línea. La diferencia práctica no está solo en escribir menos: durante el recorrido, rg aplica reglas de exclusión pensadas para repositorios. Por defecto omite archivos ignorados, ocultos y binarios, y no sigue enlaces simbólicos. grep -R no aplica automáticamente .gitignore; además, las opciones y el comportamiento recursivo pueden variar entre implementaciones de grep.
grep sigue siendo útil en scripts portables y al procesar flujos de texto. ripgrep no es un reemplazo idéntico: sus opciones, expresiones regulares y archivos examinados pueden producir resultados distintos.
fd frente a find
fd busca entradas del sistema de archivos. Su patrón se interpreta como expresión regular y, normalmente, se compara con el nombre de la entrada. Puedes restringir por extensión y tipo:
fd -t f -e ts calculateTotal
fd -t d billingLa primera orden busca archivos TypeScript; la segunda, directorios. Para comparar contra la ruta completa existe --full-path. Para patrones de shell como *.test.ts, usa --glob y conserva las comillas.
Con find, una búsqueda por nombre se expresa así:
find src tests -type f -name '*Total*.ts'No son órdenes equivalentes para cualquier árbol: fd omite normalmente entradas ocultas e ignoradas. También utiliza smart case: un patrón sin mayúsculas busca sin distinguirlas; al introducir una mayúscula, pasa a distinguirlas.
find sigue siendo una herramienta más amplia para combinar predicados sobre archivos y ejecutar acciones. No conviene sustituirlo mecánicamente por fd en scripts existentes. Los detalles de filtros y opciones están en el manual oficial de fd.
fzf: selección interactiva mediante búsqueda difusa
fzf recibe candidatos y reduce la lista mientras escribes. La búsqueda difusa permite omitir caracteres intermedios: calctot puede encontrar calculateTotal.ts porque conserva el orden de los caracteres buscados. No es un corrector ortográfico ni interpreta el significado del código.
Con las flechas cambias la selección, con Enter la confirmas y con Esc cancelas. La selección se escribe en la salida estándar, por lo que otro programa puede recibirla. También puede recorrer archivos cuando se ejecuta solo; aquí le pasaremos candidatos explícitos para controlar qué se busca.
Instalación en macOS y Linux
En macOS, con Homebrew instalado:
brew install ripgrep fd fzfEn Debian o Ubuntu, con los paquetes disponibles en los repositorios de tu distribución:
sudo apt update
sudo apt install ripgrep fd-find fzfEn Debian y Ubuntu el ejecutable de fd-find suele llamarse fdfind. Para los ejemplos puedes sustituir fd por fdfind. Si quieres usar fd también desde programas que invocan un shell nuevo, el proyecto recomienda un enlace en un directorio incluido en PATH:
mkdir -p "$HOME/.local/bin"
ln -s "$(command -v fdfind)" "$HOME/.local/bin/fd"
export PATH="$HOME/.local/bin:$PATH"Hazlo únicamente si fdfind existe y esa ruta de destino está libre. Un alias interactivo alias fd=fdfind es más simple, pero no necesariamente estará disponible en los comandos que ejecute fzf internamente.
Estas vías aparecen en las instrucciones oficiales de ripgrep, fd y fzf. Las versiones de los paquetes dependen de la distribución. Comprueba las tuyas antes de copiar una configuración:
rg --version
fd --version
fzf --versionEl laboratorio de este artículo se ejecutó en macOS ARM64 con ripgrep 15.2.0, fd 10.5.1 y fzf 0.74.5. La instalación de Linux se contrastó con las instrucciones oficiales; no se ejecutó en una distribución Linux. No hicimos mediciones comparativas de rendimiento.
Laboratorio: un repositorio TypeScript pequeño y reproducible
Necesitas Git y las tres herramientas. Los archivos TypeScript son el material de búsqueda: no necesitas instalar Node.js, paquetes npm ni un framework para completar los ejercicios.
Ejecuta este bloque en Bash o Zsh, desde una carpeta de trabajo donde no exista terminal-search-lab. git init hace que el ejemplo sea un repositorio real a efectos de las reglas de .gitignore; no crea commits ni conecta con GitHub.
mkdir terminal-search-lab
cd terminal-search-lab
git init -q
mkdir -p src/{billing,config} tests docs dist node_modules/demo
cat > .gitignore <<'EOF'
node_modules/
dist/
.env
EOF
cat > src/billing/calculateTotal.ts <<'EOF'
export function calculateTotal(prices: number[]): number {
return prices.reduce((sum, price) => sum + price, 0);
}
EOF
cat > src/billing/checkout.ts <<'EOF'
import { calculateTotal } from './calculateTotal';
export const total = calculateTotal([12, 8]);
export const currency = process.env.CURRENCY ?? 'CLP';
EOF
cat > src/config/env.ts <<'EOF'
export const apiUrl = process.env.API_URL ?? 'http://localhost:3000';
export const logLevel = process.env.LOG_LEVEL ?? 'info';
export const timeout = process.env['REQUEST_TIMEOUT'] ?? '5000';
EOF
cat > tests/calculateTotal.test.ts <<'EOF'
import { calculateTotal } from '../src/billing/calculateTotal';
if (calculateTotal([12, 8]) !== 20) throw new Error('Total incorrecto');
EOF
cat > 'docs/billing notes.md' <<'EOF'
calculateTotal recibe precios; no calcula impuestos ni descuentos.
EOF
cat > .env.example <<'EOF'
API_URL=http://localhost:3000
CURRENCY=CLP
LOG_LEVEL=info
REQUEST_TIMEOUT=5000
EOF
printf 'FAKE_TOKEN=example-only\n' > .env
printf 'calculateTotal: copia generada\n' > dist/bundle.js
printf 'calculateTotal: dependencia ficticia\n' > node_modules/demo/index.jsTodos los valores son ficticios. Incluimos a propósito una copia generada, una dependencia simulada, un archivo oculto y una ruta con espacios.
1. Encontrar archivos por nombre
Desde terminal-search-lab:
fd -t f -e ts calculateTotalSe encontraron estas dos rutas; el orden de salida puede variar:
src/billing/calculateTotal.ts
tests/calculateTotal.test.tsPara encontrar el directorio del módulo:
fd -t d billingDevuelve src/billing/. Si solo recuerdas que el archivo es TypeScript y vive dentro de src, puedes listar candidatos así:
fd -t f -e ts . srcAquí . es el patrón y src es el directorio de búsqueda. Es una distinción útil: en fd docs, docs sería un patrón de nombre, no una instrucción para recorrer únicamente esa carpeta.
2. Buscar referencias a una función
rg -n -F 'calculateTotal' src tests-F interpreta el patrón literalmente. En este laboratorio aparecen cinco líneas entre tres archivos: dos importaciones, dos llamadas y la declaración de la función.
Para buscar el nombre seguido de un paréntesis, con espacios opcionales:
rg -n 'calculateTotal\s*\(' src testsEsta expresión regular reduce el resultado a tres líneas: las dos llamadas y la declaración. No distingue entre ellas porque examina texto. Tampoco sigue un alias como import { calculateTotal as sum } para reconocer después sum(...).
Para renombrar un símbolo o conocer sus referencias semánticas, utiliza el servicio de lenguaje de TypeScript en tu editor. rg resulta muy útil para investigar, incluyendo comentarios y documentación, pero no reemplaza ese análisis.
Las expresiones se escriben entre comillas para que el shell no las expanda. El motor predeterminado de ripgrep no admite todas las construcciones de otros motores, como look-around y referencias hacia atrás. -P permite usar PCRE2 si la compilación instalada lo incluye. Puedes consultar esa diferencia en las preguntas frecuentes de ripgrep.
3. Localizar variables de entorno utilizadas
Primero, los accesos mediante notación de punto:
rg -n -g '*.ts' 'process\.env\.[A-Z_][A-Z0-9_]*' srcPara obtener solo las referencias y eliminar duplicados:
rg --no-heading -o --no-filename \
'process\.env\.[A-Z_][A-Z0-9_]*' src | sort -uSalida obtenida:
process.env.API_URL
process.env.CURRENCY
process.env.LOG_LEVELFalta deliberadamente REQUEST_TIMEOUT: está escrito con corchetes. Una segunda búsqueda permite revisar ese estilo:
rg -n -F 'process.env[' srcObtendrás la línea 3 de src/config/env.ts. La búsqueda no enumera los posibles valores de accesos dinámicos como process.env[name], ni reconoce automáticamente destructuración, wrappers o import.meta.env. Puede encontrar texto dentro de comentarios y cadenas. Es un inventario inicial para revisar, no una auditoría completa del entorno.
4. Filtrar resultados interactivamente
Para elegir entre archivos TypeScript:
fd -t f -e ts | fzfEscribe calctot, revisa los candidatos y pulsa Enter. Para elegir entre líneas que mencionan la función:
rg --line-number --no-heading --color=never \
-F 'calculateTotal' src tests | fzfAhora escribe checkout. Verás las coincidencias de ese archivo. rg ya terminó la búsqueda de contenido; escribir dentro de fzf filtra esa lista, no vuelve a ejecutar ripgrep con cada tecla.
También puedes comprobar la coincidencia difusa sin abrir la interfaz:
fd -t f -e ts | fzf --filter='calctot'En la prueba devolvió los dos archivos cuyo nombre contiene calculateTotal. --filter imprime las coincidencias y termina; sirve para automatizar comprobaciones, pero no constituye una selección interactiva. El manual de fzf detalla ambas modalidades.
5. Abrir el archivo encontrado en VS Code o Neovim
Cuando solo necesitas la ruta, pide a ripgrep nombres de archivos mediante -l:
if file=$(rg -l -F 'calculateTotal' src tests | fzf); then
code -- "$file"
fiLas comillas conservan los espacios. El if evita abrir el editor cuando cancelas o no hay selección. Con Neovim, cambia la última orden por:
nvim -- "$file"Ejecuta esa variante dentro del mismo if, en lugar de code, y con Neovim instalado. Para una ubicación conocida del laboratorio:
code --goto 'src/billing/checkout.ts:2'
# Alternativa:
nvim +2 -- src/billing/checkout.tsVS Code documenta --goto y la instalación del comando code; Neovim documenta +[num] y --. En macOS quizá debas habilitar code desde la paleta de comandos del editor.
La variante sencilla trata cada línea como una ruta. Para nombres que puedan contener saltos de línea necesitas separadores NUL de extremo a extremo. Esta función es para Bash y evita guardar un byte NUL en una variable:
fe() {
local file
if IFS= read -r -d '' file < <(
fd -t f -0 . | fzf --read0 --print0
); then
code -- "$file"
fi
}El read consume hasta NUL; el editor recibe una sola ruta entre comillas. -- separa opciones de nombres. No uses eval para abrir una selección ni code $(...) sin comillas: los nombres no deben convertirse en instrucciones o fragmentarse en varios argumentos.
Qué ocurre con .gitignore y los archivos ocultos
Prueba una búsqueda desde la raíz del laboratorio:
rg -n -F 'calculateTotal' .Se obtienen seis líneas, incluida la nota de documentación. Las copias de dist/ y node_modules/ no aparecen. Para revisar deliberadamente esas carpetas:
rg -n --no-ignore -F 'calculateTotal' dist node_modulesAhora aparecen las dos copias. Separar el origen del código generado evita confundir archivos que necesitas editar con resultados que se regenerarán.
Para comprobar el archivo de ejemplo oculto sin desactivar .gitignore:
rg -n --hidden '^API_URL=' .
fd --hidden '^\.env'La primera orden encuentra .env.example; la segunda también lo lista. El .env sigue excluido por .gitignore. En cambio:
fd --hidden --no-ignore '^\.env'lista tanto .env como .env.example.
.gitignore no es una barrera de seguridad. Un archivo explícito o determinados filtros pueden cambiar el conjunto examinado. En ripgrep, un patrón de inclusión -g puede imponerse a las reglas de ignorados: rg --hidden -g '.env*' ... no significa «solo archivos de entorno seguros para mostrar». Evita copiar resultados de archivos reales con secretos a tickets o chats.
La guía de filtrado de ripgrep también describe .ignore, .rgignore y --no-require-git. No asumas que .gitignore se aplicará igual fuera de un repositorio Git.
Atajos y aliases que no cambian toda tu terminal
Puedes empezar con dos aliases en ~/.bashrc o ~/.zshrc:
alias ff='fd --type f'
alias rgn='rg --line-number'Así conservas el comportamiento original de grep, find y rg. Para que fzf use fd al ejecutarlo sin una tubería, y también en su selector de archivos:
export FZF_DEFAULT_COMMAND='fd --type f'
export FZF_CTRL_T_COMMAND='fd --type f'Las variables no son intercambiables: la integración del shell tiene sus propias fuentes de candidatos. Si usas el nombre fdfind, ajusta ambas cadenas o configura antes el enlace descrito en la instalación.
En versiones recientes de fzf, la integración oficial se activa con una de estas opciones, según tu shell:
# Bash: en ~/.bashrc
eval "$(fzf --bash)"# Zsh: en ~/.zshrc
source <(fzf --zsh)Ese eval carga código generado por el ejecutable instalado de fzf; no ejecuta un nombre de archivo elegido. Configura las variables antes de cargar la integración y abre una terminal nueva.
Los atajos principales son Ctrl+T para insertar rutas seleccionadas, Ctrl+R para buscar en el historial y Alt+C para elegir un directorio y entrar en él. Si tu versión no reconoce --bash o --zsh, consulta los scripts incluidos por tu paquete; sus rutas varían. La combinación Alt también puede depender de la configuración del emulador de terminal.
No hace falta adoptar todos los atajos a la vez. Si ya utilizas una interfaz como LazyGit, fzf añade un selector reutilizable para otras listas, mientras LazyGit mantiene su flujo específico para Git.
Cómo trasladarlo a un repositorio grande
Antes de añadir opciones, reduce el alcance. En un monorepo, empezar por packages/billing/src puede ser más útil que revisar cada coincidencia de todo el árbol. Estas son plantillas para adaptar a las carpetas reales de tu proyecto, no resultados de un benchmark:
# Investigar una función sin incluir tests en esta primera pasada
rg -n -g '*.ts' -g '!*.test.ts' -F 'calculateTotal' src tests
# Elegir un archivo del módulo que conoces
fd -t f -e ts . src/billing | fzf
# Listar candidatos TypeScript sin instalar fd
rg --files -g '*.ts' | fzfSi necesitas comprobar el impacto de un cambio, vuelve después a los tests: excluirlos ayuda a enfocar una búsqueda, pero también oculta evidencia relevante.
Para una migración de configuración, localiza primero los usos de process.env, revisa los accesos dinámicos y contrástalos con la documentación del proyecto. Para una función compartida, busca tanto su nombre como los puntos de exportación e importación. Para localizar un archivo cuyo nombre recuerdas a medias, comienza con fd | fzf y deja la búsqueda de contenido para cuando tengas un ámbito más pequeño.
Evita activar globalmente --hidden --no-ignore. En repositorios con dependencias, compilados y cachés puede multiplicar el ruido. Tampoco conviene ejecutar vistas previas costosas sobre cada candidato sin una necesidad concreta.
Límites que conviene conocer antes de automatizar
Las tres herramientas reducen pasos manuales, pero no garantizan una búsqueda completa ni más rápida en todos los escenarios:
- Miden cosas distintas. Encontrar nombres, leer contenido y ordenar candidatos no son operaciones comparables. El tamaño de los archivos, el disco, las exclusiones y el patrón influyen en el trabajo realizado.
- Los valores predeterminados importan. ripgrep distingue mayúsculas por defecto;
fdutiliza smart case. Para ignorar mayúsculas explícitamente en una búsqueda de contenido puedes usarrg -i. - Sin coincidencias no significa error del repositorio. ripgrep devuelve 1 cuando no encuentra resultados; fzf devuelve 1 sin coincidencias y 130 al cancelar. Contempla esos estados al usar scripts con
set -e. - El texto no sustituye al lenguaje. Una coincidencia puede estar en un comentario; una referencia real puede utilizar un alias que tu patrón no cubre.
- La portabilidad sigue teniendo valor. En un servidor mínimo puede que solo estén disponibles
grepyfind. No introduzcas dependencias nuevas en un script compartido sin documentarlas. - La interfaz necesita una terminal. En CI suele ser más adecuado un filtro determinista y comprobaciones explícitas; pedir una selección manual puede dejar un job esperando.
El laboratorio comprobó las búsquedas, las exclusiones, el filtrado automático y una selección interactiva real. También se invocó VS Code mediante su CLI; la alternativa de Neovim se verificó contra su documentación, sin ejecutarla en ese entorno. No se midieron tiempos ni se extrapoló el comportamiento del ejemplo a un monorepo de producción.
Para empezar en tu siguiente tarea, prueba únicamente rg -n -F 'nombreDeFuncion' src. Cuando tengas una lista difícil de recorrer, añade fzf; cuando recuerdes el nombre del archivo y no su contenido, utiliza fd. Esa separación permite componer un flujo pequeño que entiendes y puedes ajustar a cada repositorio.
