Búsqueda y reemplazo recursivo con sed en directorios

Contexto
Estrategias para localizar y modificar cadenas de texto en múltiples archivos de forma recursiva, con capacidad de filtrar por tipo de archivo y excluir líneas comentadas.
Escenario típico
Necesidad de cambiar nombres de variables, funciones o constantes en toda una base de código
Refactorización de nombres de configuración
Migración de valores legacy a nuevas nomenclaturas
Requiere precisión para no modificar comentarios ni código inactivo
Estrategias de búsqueda
1. Localización básica (análisis previo)
# Buscar en todos los archivos
find . -type f -exec grep -l "PATRON_BUSQUEDA" {} \;
# Con contexto (3 líneas antes y después)
find . -type f -exec grep -n -B3 -A3 "PATRON_BUSQUEDA" {} + | less
# Múltiples patrones (OR lógico)
find . -type f -exec grep -l "PATRON_1\|PATRON_2\|PATRON_3" {} \;
2. Filtrado por tipo de archivo
# Solo archivos con extensión específica
find . -type f -name "*.php" -exec grep -l "PATRON" {} \;
find . -type f -name "*.js" -exec grep -l "PATRON" {} \;
find . -type f -name "*.py" -exec grep -l "PATRON" {} \;
# Múltiples extensiones
find . -type f \( -name "*.php" -o -name "*.inc" \) -exec grep -l "PATRON" {} \;
# Excluir directorios específicos
find . -type f -name "*.php" -not -path "*/vendor/*" -not -path "*/node_modules/*" \
  -exec grep -l "PATRON" {} \;
3. Análisis con numeración de líneas
# Ver archivo:línea:contenido
find . -type f -name "*.php" -exec grep -Hn "PATRON" {} \;
# Con AWK para formato personalizado
find . -type f -name "*.php" -exec awk '/PATRON/ {print FILENAME ":" NR ":" $0}' {} +
Estrategias de reemplazo
1. Reemplazo básico (SIN backup automático)
# Reemplazar en todos los archivos de un tipo
find . -type f -name "*.php" -exec sed -i 's/PATRON_VIEJO/PATRON_NUEVO/g' {} \;
# Múltiples reemplazos en una pasada
find . -type f -name "*.php" -exec sed -i '
  s/PATRON_1/NUEVO_1/g
  s/PATRON_2/NUEVO_2/g
  s/PATRON_3/NUEVO_3/g
' {} \;
Nota: 
-i modifica archivos en sitio. En BSD/macOS usar 
sed -i ''
2. Reemplazo CON backup automático
# Crea archivo.php.bak antes de modificar
find . -type f -name "*.php" -exec sed -i.bak 's/PATRON_VIEJO/PATRON_NUEVO/g' {} \;
# Limpiar backups después de verificar
find . -type f -name "*.php.bak" -delete
3. Excluir líneas comentadas
PHP/JavaScript/C-style
# Excluir líneas que empiezan por #, //, /* o *
find . -type f -name "*.php" -exec sed -i.bak '/^\s*[#*\/]/! s/PATRON/NUEVO/g' {} \;
Desglose del patrón:
/^\s*[#*\/]/! → Negación: aplica solo si NO cumple
^\s* → Inicio de línea con posibles espacios/tabs
[#*\/] → Caracteres de comentario: 
#, 
*, 
/
! → Negación lógica
Versión robusta (múltiples tipos de comentario)
find . -type f -name "*.php" -exec sed -i.bak '
/^\s*#/! {
  /^\s*\/\//! {
    /^\s*\/\*/! {
      /^\s*\*/! {
        s/PATRON_1/NUEVO_1/g
        s/PATRON_2/NUEVO_2/g
      }
    }
  }
}
' {} \;
Esto excluye:
# comentario estilo shell/Python
// comentario estilo C++/JavaScript
/* inicio de bloque comentario */
* líneas dentro de bloque comentario
Python
# Excluir líneas que empiezan por #
find . -type f -name "*.py" -exec sed -i.bak '/^\s*#/! s/PATRON/NUEVO/g' {} \;
Shell scripts
# Excluir líneas que empiezan por #
find . -type f -name "*.sh" -exec sed -i.bak '/^\s*#/! s/PATRON/NUEVO/g' {} \;
4. Alternativa con Perl (más flexible)
# Perl permite regex más complejas
find . -type f -name "*.php" -exec perl -i.bak -pe '
  s/PATRON_VIEJO/PATRON_NUEVO/g unless /^\s*[#*\/]/
' {} \;
# Múltiples patrones con hash de sustituciones
find . -type f -name "*.php" -exec perl -i.bak -pe '
  BEGIN { 
    %replace = (
      "PATRON_1" => "NUEVO_1",
      "PATRON_2" => "NUEVO_2",
      "PATRON_3" => "NUEVO_3"
    );
  }
  unless (/^\s*[#*\/]/) {
    s/$_/$replace{$_}/g for keys %replace;
  }
' {} \;
Flujo de trabajo recomendado
# PASO 1: Análisis inicial (¿qué se va a cambiar?)
find . -type f -name "*.php" -exec grep -Hn "PATRON_VIEJO" {} \; | less
# PASO 2: Análisis excluyendo comentarios
find . -type f -name "*.php" -exec awk '
  !/^\s*[#*\/]/ && /PATRON_VIEJO/ {
    print FILENAME ":" NR ":" $0
  }
' {} + | less
# PASO 3: Prueba en un solo archivo
sed -i.test '/^\s*[#*\/]/! s/PATRON_VIEJO/PATRON_NUEVO/g' ruta/archivo_prueba.php
diff ruta/archivo_prueba.php ruta/archivo_prueba.php.test
# PASO 4: Aplicar con backup (EJECUTAR)
find . -type f -name "*.php" -exec sed -i.bak '/^\s*[#*\/]/! s/PATRON_VIEJO/PATRON_NUEVO/g' {} \;
# PASO 5: Revisar cambios (si hay control de versiones)
git diff
git status
# PASO 6: Validar funcionamiento
# [Ejecutar tests, verificar aplicación]
# PASO 7: Limpiar backups si todo OK
find . -name "*.php.bak" -delete
Casos de uso comunes
Cambio de prefijo de base de datos
# MySQL/MariaDB: cambiar prefijo de tablas en dumps
find . -type f -name "*.sql" -exec sed -i.bak 's/prefijo_viejo_/prefijo_nuevo_/g' {} \;
Refactorización de nombres de clase
# PHP: cambiar nombre de clase
find . -type f -name "*.php" -exec sed -i.bak '/^\s*[#*\/]/! {
  s/class OldClassName/class NewClassName/g
  s/OldClassName::/NewClassName::/g
  s/new OldClassName/new NewClassName/g
}' {} \;
Migración de configuración
# Cambiar nombres de constantes de configuración
find . -type f \( -name "*.php" -o -name "*.inc" \) -exec sed -i.bak '/^\s*[#*\/]/! {
  s/OLD_CONFIG_NAME/NEW_CONFIG_NAME/g
  s/OLD_DB_HOST/NEW_DB_HOST/g
  s/OLD_API_KEY/NEW_API_KEY/g
}' {} \;
Actualización de rutas
# Cambiar rutas legacy
find . -type f -name "*.php" -exec sed -i.bak '/^\s*[#*\/]/! {
  s|/var/www/old_path/|/var/www/new_path/|g
  s|old-domain\.com|new-domain.com|g
}' {} \;
Limitaciones conocidas
1. Comentarios multilínea
El método basado en 
^\s* solo detecta comentarios al inicio de línea. No detecta:
$var = "value"; /* comentario inline */ $otra = "PATRON"; // no se excluirá
Para estos casos, considerar un parser específico del lenguaje.
2. Cadenas literales
Si 
PATRON aparece dentro de strings, será reemplazado:
$sql = "SELECT * FROM old_table"; // se cambiará "old_table"
Para evitarlo se requiere análisis sintáctico completo.
3. Bloques multilínea
Comentarios que abarcan múltiples líneas:
/*
 * Este es un comentario
 * con PATRON_VIEJO que
 * no será detectado en líneas 2-3
 */
Solo la primera línea (
/*) se excluirá.
Alternativas avanzadas
Ripgrep (rg) + sd
# Búsqueda ultrarrápida
rg "PATRON" --type php
# Reemplazo con sd (más intuitivo que sed)
find . -name "*.php" -exec sd "PATRON_VIEJO" "PATRON_NUEVO" {} \;
Usando AG (The Silver Searcher)
# Listar archivos
ag -l "PATRON" --php
# Con contexto
ag "PATRON" --php -C 3
Scripts dedicados por lenguaje
Para refactorizaciones complejas, usar herramientas especializadas:
PHP: PHP-CS-Fixer, Rector
JavaScript: jscodeshift, lebab
Python: rope, bowler
Verificación post-cambio
# Contar ocurrencias antes y después
echo "Antes:"
find . -name "*.php" -exec grep -o "PATRON_VIEJO" {} \; | wc -l
echo "Después:"
find . -name "*.php" -exec grep -o "PATRON_NUEVO" {} \; | wc -l
# Verificar que no quedan ocurrencias del patrón viejo
find . -name "*.php" -exec grep -H "PATRON_VIEJO" {} \;
# Si hay control de versiones
git diff --stat
git diff | grep -E "^\+.*PATRON_NUEVO" | wc -l
Consejos de seguridad
Siempre hacer backup antes de modificaciones masivas
Usar control de versiones (git commit antes de ejecutar)
Probar en un archivo antes de aplicar a todo el proyecto
Revisar diff completo antes de commit final
Ejecutar suite de tests después de cambios
Considerar crear rama temporal para refactorizaciones grandes
Troubleshooting
"Device or resource busy"
# Puede ocurrir con archivos abiertos. Solución: cerrar editores
lsof | grep nombre_archivo
"Permission denied"
# Verificar permisos
find . -type f -name "*.php" ! -writable
# Corregir si es necesario
find . -type f -name "*.php" -exec chmod u+w {} \;
Sed no modifica archivos
# Verificar sintaxis específica del sistema
sed --version  # GNU sed
sed -i.bak ...  # funciona en GNU
# En macOS/BSD
sed -i '' ...   # requiere argumento vacío
Última actualización: Octubre 2025
Compatibilidad: Linux (GNU sed), *BSD/macOS (requiere ajustes en 
-i)
Herramientas: find, sed, grep, awk, perl