¿Cómo hacer un reemplazo de texto en una jerarquía de carpetas grandes?

11

Quiero buscar y reemplazar texto en un gran conjunto de archivos, excluyendo algunas instancias. Para cada línea, quiero un aviso que me pregunte si necesito reemplazar esa línea o no. Algo similar a los de vim :%s/from/to/gc(con la csolicitud de confirmación), pero en un conjunto de carpetas. ¿Hay alguna buena herramienta de línea de comandos o script que pueda usarse?

balki
fuente
Sobre la importancia de un formato adecuado: inicialmente leí su comando como s/from/to/gcon un error de formato después, en lugar de hacer s/from/to/gchincapié en lo cque había intentado escribir (no puede hacerlo con Markdown, puede hacerlo con <code>y <strong>etiquetas HTML).
Gilles 'SO- deja de ser malvado'

Respuestas:

19

¿Por qué no usar vim?

Abra todos los archivos en vim

vim $(find . -type f)

O abra solo archivos relevantes (como lo sugiere Caleb)

vim $(grep 'from' . -Rl)

Y luego ejecute el reemplazo en todos los búferes

:bufdo %s/from/to/gc | update

También puedes hacerlo sed, pero mi conocimiento de sed es limitado.

Gert
fuente
Gracias, su respuesta me hizo hacer una doble toma tardía: me di cuenta de que había perdido completamente el bit interactivo. No creo que eso sea posible con sed (no hay suficientes canales de entrada / salida).
Gilles 'SO- deja de ser malvado'
1
Puede acelerar esto al no abrir TODOS los archivos en el búfer actual utilizando en greplugar de findabrir solo archivos que tengan coincidencias conocidas. vim $(grep 'from' . -Rl)
Caleb
Gracias. ¿Se necesita c (astreriks alrededor de c)? o es un problema de formato?
balki
@balki es un problema de "formateo". Solucionado
Gert
5

Puede hacer algo crudo con un pequeño script de Perl que recibe instrucciones de realizar reemplazos línea por línea ( -l -pe) en los archivos pasados ​​como argumentos ( -i):

perl -i -l -pe '
    if (/from/) {                            # is the source text present on this line?
        printf STDERR ("%s: %s [y/N]? ", $ARGV, $_);  # display a prompt
        $r=<STDIN>;                                   # read user response
        if ($r =~ /^[Yy]/) {                          # if user entered Y:
            s/from/to/g;                              # replace all occurences on this line
    }' /path/to/files

Las posibles mejoras serían colorear partes de la solicitud y apoyar cosas como "reemplazar todas las ocurrencias en el archivo actual". Solicitar por separado cada aparición en una línea sería más difícil.

Segunda parte, haciendo coincidir los archivos. Si no hay demasiados archivos involucrados y está ejecutando zsh, puede hacer coincidir todos los archivos en el directorio actual y sus subdirectorios de forma recursiva:

perl -i -l -pe '…' **/*(.)

Si su shell es bash ≥4, puede ejecutar perl … **/*, pero eso producirá mensajes de error espurios porque sed intentará (y fallará) ejecutar en directorios. Si solo desea realizar el reemplazo en un conjunto de archivos como archivos C, puede restringir las coincidencias (que funciona en bash ≥4 o zsh):

perl -i -l -pe '…' **/*.[hc]

Si necesita un control más preciso sobre qué archivos está reemplazando, o su shell no tiene la construcción de coincidencia de directorio recursiva **, o si tiene demasiados archivos y obtiene un error de "línea de comando demasiado larga", use find. Por ejemplo, para realizar un reemplazo en todos los archivos nombrados *.ho *.cen el directorio actual y sus subdirectorios (en sistemas más antiguos, es posible que deba usar en \;lugar de +al final de la línea (el +formulario es más rápido pero no está disponible en todas partes).

find . -type f -name '*.[hc]' -exec perl -i -l -pe '…' {} +

Dicho esto, me quedaría con un editor interactivo si necesita interacción. Gert ha mostrado una forma de hacerlo en Vim , aunque requiere abrir todos los archivos que desea buscar, lo que puede ser un problema si hay muchos.

En Emacs, así es como puedes hacer esto:

  1. Reúna los nombres de archivo con M-x find-name-dired(especifique un directorio de nivel superior) o M-x find-dired(especifique una findlínea de comando arbitraria ).
  2. En el búfer dired resultante , presione tpara marcar todos los archivos, luego Q( dired-do-query-replace-regexp) para realizar un reemplazo con una solicitud en los archivos marcados.
Gilles 'SO- deja de ser malvado'
fuente
1

sdiff(ver http://www.gnu.org/software/diffutils/manual/diffutils.html#Invoking-sdiff ) podría ser útil aquí. Con él puedes hacer parches interactivos. Entonces, hacerlo con un archivo temporal que creó al realizar operaciones de reemplazo sedpodría ser una posible solución:

# use file descriptor 3 to still allow use of stdin
while IFS= read -r -d '' file <&3; do

  # write the result of the replacement into a temporary file
  sed -r 's/something/something_else/g' -- "$file" > replacer_tmp

  if cmp -s -- "$file" replacer_tmp; then
    continue; # nothing was replaced
  fi

  echo "There is something to replace in '$file'! Starting interactive diff."
  echo

  sdiff -o "$file" -s -d -- "$file" replacer_tmp

  echo

done 3< <(find . -type f -print0)

(Bucle de archivos que utiliza la sustitución de procesos que no son POSIX y read -dsegún lo respaldado, por ejemplo, por bash).

phk
fuente