Skip to main content

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