¿Cómo puedo reemplazar una cadena en un archivo (s)?


751

Reemplazar cadenas en archivos basados ​​en ciertos criterios de búsqueda es una tarea muy común. Cómo puedo

  • reemplazar cadena foocon baren todos los archivos en el directorio actual?
  • hacer lo mismo recursivamente para subdirectorios?
  • reemplazar solo si el nombre del archivo coincide con otra cadena?
  • reemplazar solo si la cadena se encuentra en un determinado contexto?
  • reemplazar si la cadena está en un cierto número de línea?
  • reemplazar múltiples cadenas con el mismo reemplazo
  • reemplazar múltiples cadenas con diferentes reemplazos

2
Esto pretende ser un Q&A canónico sobre este tema (vea esta meta discusión ), no dude en editar mi respuesta a continuación o agregar la suya.
terdon

Respuestas:


1009

1. Reemplazar todas las ocurrencias de una cadena con otra en todos los archivos en el directorio actual:

Estos son para casos en los que sabe que el directorio contiene solo archivos regulares y que desea procesar todos los archivos no ocultos. Si ese no es el caso, use los enfoques en 2.

Todas las sedsoluciones en esta respuesta asumen GNU sed. Si usa FreeBSD o OS / X, reemplácelo -icon -i ''. También tenga en cuenta que el uso del -iconmutador con cualquier versión de sedtiene ciertas implicaciones de seguridad del sistema de archivos y no es aconsejable en cualquier script que planee distribuir de ninguna manera.

  • Archivos no recursivos en este directorio solamente:

    sed -i -- 's/foo/bar/g' *
    perl -i -pe 's/foo/bar/g' ./* 
    

    (el perlque fallará para los nombres de archivo que terminan en |o espacio) ).

  • Archivos regulares y recursivos ( incluidos los ocultos ) en este y todos los subdirectorios

    find . -type f -exec sed -i 's/foo/bar/g' {} +

    Si está usando zsh:

    sed -i -- 's/foo/bar/g' **/*(D.)

    (puede fallar si la lista es demasiado grande, ver zargspara evitar).

    Bash no puede verificar directamente los archivos normales, se necesita un bucle (los paréntesis evitan configurar las opciones globalmente):

    ( shopt -s globstar dotglob;
        for file in **; do
            if [[ -f $file ]] && [[ -w $file ]]; then
                sed -i -- 's/foo/bar/g' "$file"
            fi
        done
    )
    

    Los archivos se seleccionan cuando son archivos reales (-f) y se pueden escribir (-w).

2. Reemplace solo si el nombre del archivo coincide con otra cadena / tiene una extensión específica / es de cierto tipo, etc.

  • Archivos no recursivos en este directorio solamente:

    sed -i -- 's/foo/bar/g' *baz*    ## all files whose name contains baz
    sed -i -- 's/foo/bar/g' *.baz    ## files ending in .baz
    
  • Archivos regulares y recursivos en este y todos los subdirectorios

    find . -type f -name "*baz*" -exec sed -i 's/foo/bar/g' {} +

    Si está utilizando bash (las llaves evitan configurar las opciones globalmente):

    ( shopt -s globstar dotglob
        sed -i -- 's/foo/bar/g' **baz*
        sed -i -- 's/foo/bar/g' **.baz
    )
    

    Si está usando zsh:

    sed -i -- 's/foo/bar/g' **/*baz*(D.)
    sed -i -- 's/foo/bar/g' **/*.baz(D.)
    

    El --sirve para decir sedque no se darán más banderas en la línea de comando. Esto es útil para proteger contra los nombres de archivos que comienzan con -.

  • Si un archivo es de cierto tipo, por ejemplo, ejecutable (ver man findmás opciones):

    find . -type f -executable -exec sed -i 's/foo/bar/g' {} +

    zsh:

    sed -i -- 's/foo/bar/g' **/*(D*)

3. Reemplace solo si la cadena se encuentra en un contexto determinado

  • Reemplace foocon barsolo si hay un bazposterior en la misma línea:

    sed -i 's/foo\(.*baz\)/bar\1/' file

    En sed, el uso de \( \)guardar lo que está entre paréntesis y luego puede acceder a él con \1. Hay muchas variaciones de este tema, para obtener más información sobre tales expresiones regulares, consulte aquí .

  • Reemplace foocon barsolo si foose encuentra en la columna 3d (campo) del archivo de entrada (suponiendo campos separados por espacios en blanco):

    gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file

    (necesita gawk4.1.0 o más reciente).

  • Para un campo diferente, simplemente use $Nwhere Nes el número del campo de interés. Para un separador de campo diferente ( :en este ejemplo) use:

    gawk -i inplace -F':' '{gsub(/foo/,"baz",$3);print}' file

    Otra solución usando perl:

    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 

    NOTA: tanto el awky perlsoluciones afectará espaciado en el archivo (eliminar los espacios en blanco de ataque y salida, y convertir secuencias de espacios en blanco para un carácter de espacio en aquellas líneas que responden). Para un campo diferente, use $F[N-1]dónde Nestá el número de campo que desea y para un separador de campo diferente ( $"=":"establece el separador de campo de salida en :):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • Reemplace foocon barsolo en la cuarta línea:

    sed -i '4s/foo/bar/g' file
    gawk -i inplace 'NR==4{gsub(/foo/,"baz")};1' file
    perl -i -pe 's/foo/bar/g if $.==4' file
    

4. Operaciones de reemplazo múltiple: reemplace con diferentes cadenas

  • Puedes combinar sedcomandos:

    sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file

    Tenga en cuenta que el orden es importante ( sed 's/foo/bar/g; s/bar/baz/g'sustituirá foocon baz).

  • o comandos de Perl

    perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
  • Si tiene una gran cantidad de patrones, es más fácil guardar sus patrones y sus reemplazos en un sedarchivo de script:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
    
  • O, si tiene demasiados pares de patrones para que lo anterior sea factible, puede leer los pares de patrones de un archivo (dos patrones separados por espacios, $ patrón y $ reemplazo, por línea):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
    
  • Eso será bastante lento para largas listas de patrones y archivos de datos grandes, por lo que es posible que desee leer los patrones y crear un sedscript a partir de ellos. Lo siguiente supone que un delimitador <space> separa una lista de pares MATCH <space> REPLACE que ocurren uno por línea en el archivo patterns.txt:

    sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' <patterns.txt |
    sed -f- ./editfile >outfile
    

    El formato anterior es en gran medida arbitrario y, por ejemplo, no permite un <space> en MATCH o REPLACE . Sin embargo, el método es muy general: básicamente, si puede crear una secuencia de salida que se parezca a una sedsecuencia de comandos, puede generar esa secuencia como sedsecuencia de comandos especificando sedel archivo de secuencia de comandos como -stdin.

  • Puede combinar y concatenar múltiples scripts de manera similar:

    SOME_PIPELINE |
    sed -e'#some expression script'  \
        -f./script_file -f-          \
        -e'#more inline expressions' \
    ./actual_edit_file >./outfile
    

    Un POSIX sedconcatenará todos los scripts en uno en el orden en que aparecen en la línea de comandos. Ninguno de estos debe terminar en una línea \nelectrónica.

  • grep puede funcionar de la misma manera:

    sed -e'#generate a pattern list' <in |
    grep -f- ./grepped_file
    
  • Cuando se trabaja con cadenas fijas como patrones, es una buena práctica escapar de los metacaracteres de expresiones regulares . Puedes hacer esto con bastante facilidad:

    sed 's/[]$&^*\./[]/\\&/g
         s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
    ' <patterns.txt |
    sed -f- ./editfile >outfile
    

5. Operaciones de reemplazo múltiple: reemplaza múltiples patrones con la misma cadena

  • Reemplace cualquiera de foo, baro bazconfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • o

    perl -i -pe 's/foo|bar|baz/foobar/g' file

2
@ StéphaneChazelas gracias por la edición, de hecho solucionó varias cosas. Sin embargo, no elimine la información relevante para bash. No todos lo usan zsh. Por supuesto, agregue zshinformación, pero no hay ninguna razón para eliminar las cosas de bash. Además, sé que usar el shell para el procesamiento de texto no es ideal, pero hay casos en los que es necesario. Edité una versión mejor de mi script original que creará un sedscript en lugar de usar el bucle de shell para analizar. Esto puede ser útil si tiene varios cientos de pares de patrones, por ejemplo.
terdon

2
@terdon, tu bash one es incorrecto. bash antes de 4.3 seguirá los enlaces simbólicos al descender. Además, bash no tiene equivalente para el (.)calificador global, por lo que no se puede usar aquí. (también te estás perdiendo algo). El bucle for es incorrecto (falta -r) y significa realizar varias pasadas en los archivos y no agrega ningún beneficio sobre un script sed.
Stéphane Chazelas

77
@terdon ¿Qué indica --después sed -iy antes del comando sustituto?
Geek

55
@ Geek eso es una cosa POSIX. Significa el final de las opciones y le permite pasar argumentos que comienzan con -. Su uso asegura que los comandos funcionarán en archivos con nombres como -foo. Sin él, el -fsería analizado como una opción.
terdon

1
Tenga mucho cuidado al ejecutar algunos de los comandos recursivos en los repositorios de git. Por ejemplo, las soluciones proporcionadas en la sección 1 de esta respuesta en realidad modificarán los archivos internos de git en un .gitdirectorio, y realmente estropearán su pago. Es mejor operar dentro / sobre directorios específicos por nombre.
Pistos

75

Una buena r e pl acement herramienta de Linux es RPL , que fue escrito originalmente para el proyecto Debian, por lo que está disponible con apt-get install rplen cualquier distro derivada de Debian, y puede ser para los demás, pero por lo demás se puede descargar el tar.gzarchivo en SourgeForge .

El ejemplo más simple de uso:

 $ rpl old_string new_string test.txt

Tenga en cuenta que si la cadena contiene espacios, debe estar entre comillas. De forma predeterminada, rplse ocupan de las letras mayúsculas pero no de las palabras completas , pero puede cambiar estos valores predeterminados con opciones -i(ignorar mayúsculas y minúsculas) y -w(palabras completas). También puede especificar múltiples archivos :

 $ rpl -i -w "old string" "new string" test.txt test2.txt

O incluso especifique las extensiones ( -x) para buscar o incluso busque recursivamente ( -R) en el directorio:

 $ rpl -x .html -x .txt -R old_string new_string test*

También puede buscar / reemplazar en modo interactivo con -pla opción (solicitud):

El resultado muestra los números de archivos / cadenas reemplazados y el tipo de búsqueda (mayúsculas / minúsculas, palabras completas / parciales), pero puede ser silencioso con la opción -q( modo silencioso ), o incluso más detallado, enumerando números de línea que contienen coincidencias de cada archivo y directorio con la opción -v( modo detallado ).

Otras opciones que vale la pena recordar son -e(honor e scapes) que permiten regular expressions, por lo que puede buscar también pestañas ( \t), nuevas líneas ( \n), etc. Incluso puede usar -fpara forzar permisos (por supuesto, solo cuando el usuario tiene permisos de escritura) y -dpara preservar los tiempos de modificación ').

Finalmente, si no está seguro de cuál será exactamente, use el -s( modo de simulación ).


2
Mucho mejor en la retroalimentación y la simplicidad que sed. Solo desearía que permitiera actuar sobre nombres de archivos, y luego sería perfecto tal como está.
Kzqai

1
me gusta el -s (modo de simulación) :-)
erm3nda

25

Cómo hacer una búsqueda y reemplazar varios archivos sugiere:

También podría usar find y sed, pero creo que esta pequeña línea de perl funciona muy bien.

perl -pi -w -e 's/search/replace/g;' *.php
  • -e significa ejecutar la siguiente línea de código.
  • -i significa editar en el lugar
  • -w escribir advertencias
  • -p recorre el archivo de entrada, imprimiendo cada línea después de que se le aplica el script.

Mis mejores resultados provienen del uso de perl y grep (para asegurar que el archivo tenga la expresión de búsqueda)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )

13

Puede usar Vim en modo Ex:

reemplazar cadena ALF con BRA en todos los archivos en el directorio actual?

for CHA in *
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

hacer lo mismo recursivamente para subdirectorios?

find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'

reemplazar solo si el nombre del archivo coincide con otra cadena?

for CHA in *.txt
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

reemplazar solo si la cadena se encuentra en un determinado contexto?

ex -sc 'g/DEL/s/ALF/BRA/g' -cx file

reemplazar si la cadena está en un cierto número de línea?

ex -sc '2s/ALF/BRA/g' -cx file

reemplazar múltiples cadenas con el mismo reemplazo

ex -sc '%s/\vALF|ECH/BRA/g' -cx file

reemplazar múltiples cadenas con diferentes reemplazos

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file

13

Usé esto:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. Lista todos los archivos que contienen old_string.

  2. Reemplace la nueva línea en el resultado con espacios (para poder alimentar la lista de archivos sed.

  3. Ejecutar seden esos archivos para reemplazar la cadena vieja con nueva.

Actualización: el resultado anterior fallará en los nombres de archivo que contienen espacios en blanco. En cambio, use:

grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'


Tenga en cuenta que esto fallará si alguno de sus nombres de archivo contiene espacios, pestañas o líneas nuevas. El uso grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'lo hará tratar con nombres de archivos arbitrarios.
terdon

gracias chicos. agregó una actualización y dejó el código anterior porque es una advertencia interesante que podría ser útil para alguien que no conoce este comportamiento.
o_o_o--

6

Desde la perspectiva del usuario, una herramienta Unix agradable y simple que hace el trabajo perfectamente es qsubst. Por ejemplo,

% qsubst foo bar *.c *.h

reemplazará foocon baren todos mis archivos C. Una buena característica es que qsubsthará un reemplazo de consulta , es decir, me mostrará cada aparición fooy me preguntará si quiero reemplazarlo o no. [Puede reemplazar incondicionalmente (sin preguntar) con la -goopción, y hay otras opciones, por ejemplo, -wsi solo desea reemplazar foocuando es una palabra completa.]

Cómo conseguirlo: qsubstfue inventado por der Mouse (de McGill) y publicado en comp.unix.sources 11 (7) en agosto de 1987. Existen versiones actualizadas. Por ejemplo, la versión de NetBSD se qsubst.c,v 1.8 2004/11/01compila y se ejecuta perfectamente en mi Mac.


2

Necesitaba algo que podría proporcionar una opción de funcionamiento en seco y trabajaría de forma recursiva con un pegote, y después de probar a hacerlo con awky sedme di por vencido y en su lugar hice en pitón.

El script busca recursivamente todos los archivos que coinciden con un patrón global (por ejemplo --glob="*.html") para una expresión regular y la reemplaza con la expresión regular de reemplazo:

find_replace.py [--dir=my_folder] \
    --search-regex=<search_regex> \
    --replace-regex=<replace_regex> \
    --glob=[glob_pattern] \
    --dry-run

Cada opción larga como --search-regextiene una opción corta correspondiente, es decir -s. Ejecute con -hpara ver todas las opciones.

Por ejemplo, esto cambiará todas las fechas de 2017-12-31a 31-12-2017:

python replace.py --glob=myfile.txt \
    --search-regex="(\d{4})-(\d{2})-(\d{2})" \
    --replace-regex="\3-\2-\1" \
    --dry-run --verbose
import os
import fnmatch
import sys
import shutil
import re

import argparse

def find_replace(cfg):
    search_pattern = re.compile(cfg.search_regex)

    if cfg.dry_run:
        print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!')

    for path, dirs, files in os.walk(os.path.abspath(cfg.dir)):
        for filename in fnmatch.filter(files, cfg.glob):

            if cfg.print_parent_folder:
                pardir = os.path.normpath(os.path.join(path, '..'))
                pardir = os.path.split(pardir)[-1]
                print('[%s]' % pardir)
            filepath = os.path.join(path, filename)

            # backup original file
            if cfg.create_backup:
                backup_path = filepath + '.bak'

                while os.path.exists(backup_path):
                    backup_path += '.bak'
                print('DBG: creating backup', backup_path)
                shutil.copyfile(filepath, backup_path)

            with open(filepath) as f:
                old_text = f.read()

            all_matches = search_pattern.findall(old_text)

            if all_matches:

                print('Found {} matches in file {}'.format(len(all_matches), filename))

                new_text = search_pattern.sub(cfg.replace_regex, old_text)

                if not cfg.dry_run:
                    with open(filepath, "w") as f:
                        print('DBG: replacing in file', filepath)
                        f.write(new_text)
                else:
                    for idx, matches in enumerate(all_matches):
                        print("Match #{}: {}".format(idx, matches))

                    print("NEW TEXT:\n{}".format(new_text))

            elif cfg.verbose:
                print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex))


if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='''DESCRIPTION:
    Find and replace recursively from the given folder using regular expressions''',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog='''USAGE:
    {0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern]

    '''.format(os.path.basename(sys.argv[0])))

    parser.add_argument('--dir', '-d',
                        help='folder to search in; by default current folder',
                        default='.')

    parser.add_argument('--search-regex', '-s',
                        help='search regex',
                        required=True)

    parser.add_argument('--replace-regex', '-r',
                        help='replacement regex',
                        required=True)

    parser.add_argument('--glob', '-g',
                        help='glob pattern, i.e. *.html',
                        default="*.*")

    parser.add_argument('--dry-run', '-dr',
                        action='store_true',
                        help="don't replace anything just show what is going to be done",
                        default=False)

    parser.add_argument('--create-backup', '-b',
                        action='store_true',
                        help='Create backup files',
                        default=False)

    parser.add_argument('--verbose', '-v',
                        action='store_true',
                        help="Show files which don't match the search regex",
                        default=False)

    parser.add_argument('--print-parent-folder', '-p',
                        action='store_true',
                        help="Show the parent info for debug",
                        default=False)

    config = parser.parse_args(sys.argv[1:])

    find_replace(config)

Here es una versión actualizada del script que resalta los términos de búsqueda y los reemplazos con diferentes colores.


1
No entiendo por qué harías algo tan complejo. Para la recursividad, use la globstaropción de bash (o el equivalente de su shell) y **globs o find. Para una carrera en seco, solo use sed. A menos que use la -iopción, no hará ningún cambio. Para uso de respaldo sed -i.bak(o perl -i .bak); para archivos que no coinciden, use grep PATTERN file || echo file. ¿Y por qué en el mundo tendrías a Python expandir el globo en lugar de dejar que el shell lo haga? ¿Por qué en script.py --glob=foo*lugar de solo script.py foo*?
terdon

1
Mis razones son muy simples: (1) sobre todo, facilidad de depuración; (2) el uso de una sola herramienta bien documentado con una comunidad de apoyo (3) sin saber sedy awkbien y no estar dispuesto a invertir tiempo extra en el dominio de ellos, (4) la legibilidad, (5) esta solución también trabajará en sistemas no POSIX (No es que lo necesite, pero alguien más podría hacerlo).
ccpizza el

1

ripgrep (nombre del comando rg) es una grepherramienta, pero también admite la búsqueda y el reemplazo.

$ cat ip.txt
dark blue and light blue
light orange
blue sky
$ # by default, line number is displayed if output destination is stdout
$ # by default, only lines that matched the given pattern is displayed
$ # 'blue' is search pattern and -r 'red' is replacement string
$ rg 'blue' -r 'red' ip.txt
1:dark red and light red
3:red sky

$ # --passthru option is useful to print all lines, whether or not it matched
$ # -N will disable line number prefix
$ # this command is similar to: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'blue' -r 'red' ip.txt
dark red and light red
light orange
red sky


rg no admite la opción in situ, por lo que deberá hacerlo usted mismo

$ # -N isn't needed here as output destination is a file
$ rg --passthru 'blue' -r 'red' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
dark red and light red
light orange
red sky


Consulte la documentación de Rust regex para conocer la sintaxis y las características de las expresiones regulares. El -Pinterruptor habilitará el sabor PCRE2 . rgadmite Unicode de forma predeterminada.

$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # unicode support
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)

$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.

$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map


Al igual que grepla -Fopción, permitirá que las cadenas fijas coincidan, una opción práctica que creo que también seddebería implementarse.

$ printf '2.3/[4]*6\nfoo\n5.3-[4]*9\n' | rg --passthru -F '[4]*' -r '2'
2.3/26
foo
5.3-29


Otra opción práctica es la -Uque permite la coincidencia de líneas múltiples

$ # (?s) flag will allow . to match newline characters as well
$ printf '42\nHi there\nHave a Nice Day' | rg --passthru -U '(?s)the.*ice' -r ''
42
Hi  Day


rg también puede manejar archivos de estilo dos

$ # same as: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123'
hi 123
good 123


Otra ventaja de rges que es probable que sea más rápido quesed

$ # for small files, initial processing time of rg is a large component
$ time echo 'aba' | sed 's/a/b/g' > f1
real    0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real    0m0.007s

$ # for larger files, rg is likely to be faster
$ # 6.2M sample ASCII file
$ wget https://norvig.com/big.txt    
$ time LC_ALL=C sed 's/\bcat\b/dog/g' big.txt > f1
real    0m0.060s
$ time rg --passthru '\bcat\b' -r 'dog' big.txt > f2
real    0m0.048s
$ diff -s f1 f2
Files f1 and f2 are identical

$ time LC_ALL=C sed -E 's/\b(\w+)(\s+\1)+\b/\1/g' big.txt > f1
real    0m0.725s
$ time rg --no-pcre2-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.