Buscar y reemplazar texto en muchos archivos con grep y sed

Buscar un texto en todos los archivos de un proyecto y cambiarlo por otro es una línea de Bash: grep encuentra y sed cambia. Aquí tienes cómo hacerlo sin romper nada: probar antes de aplicar, guardar una copia, tratar el patrón como texto literal, sobrevivir a los nombres con espacios y a los caracteres especiales, y la diferencia de sed -i en macOS.

Supongamos que una web se muda de http://www.example.com a https://www.example.com y hay que cambiar la dirección en todos los archivos de configuración. Esta es la carpeta:

terminal$ find . -type f | sort
./config/app.conf
./config/base.conf
./notas.txt
./web/index.html

Buscar: grep -r

terminal$ grep -rn 'http://www.example.com' . | sort
./config/app.conf:2:url=http://www.example.com/app
./config/base.conf:2:url_respaldo=http://www.example.com/respaldo
./web/index.html:1:<a href="http://www.example.com/">Inicio</a>
$ grep -rl 'http://www.example.com' . | sort
./config/app.conf
./config/base.conf
./web/index.html
$ grep -rc 'example' --include='*.conf' . | sort
./config/app.conf:1
./config/base.conf:2

-r busca en la carpeta y todas sus subcarpetas, -n añade el número de línea, -l lista solo los archivos que contienen el texto y -c cuenta las coincidencias de cada archivo. --include limita la búsqueda a los nombres que encajan con un patrón (y --exclude hace lo contrario). El | sort solo está para que el orden sea siempre el mismo.

Cuidado: el patrón es una expresión regular

Para grep, el punto significa «cualquier carácter», y además busca el texto en cualquier parte de la línea, también dentro de palabras más largas:

terminal$ grep -n 'example.com' notas.txt
1:La web vieja era example.company
2:El correo va a example.com
$ grep -nw 'example.com' notas.txt
2:El correo va a example.com
$ grep -nF 'example.com' notas.txt
1:La web vieja era example.company
2:El correo va a example.com
$ grep -nwF 'example.com' notas.txt
2:El correo va a example.com

-w exige que la coincidencia sea una palabra completa, y -F trata el patrón como texto literal, sin expresiones regulares. Cuando buscas una dirección, una ruta o cualquier cosa con puntos, -F es lo más seguro.

Reemplazar: probar con sed y luego sed -i

sed 's/viejo/nuevo/g' cambia todas las apariciones (la g del final; sin ella, solo la primera de cada línea). Sin -i, sed imprime el resultado y no toca el archivo, que es justo lo que quieres para probar:

terminal$ sed 's|http://www\.example\.com|https://www.example.com|g' config/app.conf
nombre=Tienda de prueba
url=https://www.example.com/app
registro=/var/log/app.log
$ cat config/app.conf
nombre=Tienda de prueba
url=http://www.example.com/app
registro=/var/log/app.log

Como el texto tiene barras, se usa otro separador: s|…|…| funciona igual que s/…/…/ y evita tener que escapar cada /. Los puntos del patrón van escapados, \., para que signifiquen un punto de verdad. Cuando el resultado es el que quieres, -i modifica el archivo, y -i.bak guarda antes una copia con esa extensión:

terminal$ sed -i.bak 's|http://www\.example\.com|https://www.example.com|g' config/app.conf
$ diff config/app.conf.bak config/app.conf
2c2
< url=http://www.example.com/app
---
> url=https://www.example.com/app

En todos los archivos a la vez

Se combinan las dos cosas: grep -rl elige los archivos que contienen el texto y xargs se los pasa a sed -i. Con -Z en grep y -0 en xargs, los nombres viajan separados por un carácter nulo, así que funcionan aunque tengan espacios o saltos de línea:

terminal$ grep -rlZF 'http://www.example.com' . | xargs -0 -r sed -i.bak 's|http://www\.example\.com|https://www.example.com|g'
$ grep -rn 'example.com' --exclude='*.bak' . | sort
./config/app.conf:2:url=https://www.example.com/app
./config/base.conf:1:servidor=db.example.com
./config/base.conf:2:url_respaldo=https://www.example.com/respaldo
./notas.txt:1:La web vieja era example.company
./notas.txt:2:El correo va a example.com
./web/index.html:1:<a href="https://www.example.com/">Inicio</a>
./web/index.html:2:<p>Escríbenos a hola@example.com</p>

La -r de xargs (de GNU) evita que ejecute sed sin archivos si grep no encontró nada.

Los caracteres especiales en el reemplazo

En la parte del reemplazo, & significa «el texto que coincidió», y eso da sorpresas:

terminal$ echo "Llama a la compañía AT" | sed 's/AT/AT&T/'
Llama a la compañía ATATT
$ echo "Llama a la compañía AT" | sed 's/AT/AT\&T/'
Llama a la compañía AT&T

El problema crece cuando el texto viejo o el nuevo vienen de variables, porque pueden contener /, &, puntos o corchetes. Este script los escapa antes de construir la orden de sed, busca con grep -F (texto literal) y deja una copia .bak de cada archivo cambiado:

#!/usr/bin/env bash
# Uso: reemplazar.sh TEXTO_VIEJO TEXTO_NUEVO
# Cambia el texto en todos los archivos de la carpeta actual y sus subcarpetas, guardando una copia .bak de cada uno.
if (( $# != 2 )); then
    echo "Uso: ${0##*/} TEXTO_VIEJO TEXTO_NUEVO" >&2
    exit 2
fi

# Escapa lo que sed interpretaría: en el patrón, los caracteres especiales de las expresiones regulares básicas y la
# barra; en el reemplazo, la barra, la barra invertida y el &.
escapar_patron()    { printf '%s' "$1" | sed -e 's/[]\/$*.^[]/\\&/g'; }
escapar_reemplazo() { printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'; }

viejo=$(escapar_patron "$1")
nuevo=$(escapar_reemplazo "$2")

grep -rlZF --exclude='*.bak' -- "$1" . | xargs -0 -r sed -i.bak -e "s/$viejo/$nuevo/g"

echo "Archivos cambiados:"
find . -name '*.bak' -print | sed 's/\.bak$//' | sort
terminal$ bash reemplazar.sh 'http://www.example.com' 'https://www.example.com'
Archivos cambiados:
./config/app.conf
./config/base.conf
./web/index.html
$ grep -rn 'example.com' --exclude='*.bak' . | sort
./config/app.conf:2:url=https://www.example.com/app
./config/base.conf:1:servidor=db.example.com
./config/base.conf:2:url_respaldo=https://www.example.com/respaldo
./notas.txt:1:La web vieja era example.company
./notas.txt:2:El correo va a example.com
./web/index.html:1:<a href="https://www.example.com/">Inicio</a>
./web/index.html:2:<p>Escríbenos a hola@example.com</p>

sed -i en macOS

El sed de macOS es el de BSD, y su -i exige siempre una extensión para la copia: sed -i '' 's/a/b/' archivo para no hacerla. Al revés, sed -i '' falla en Linux. La forma que funciona en los dos es la de esta página, sed -i.bak, pegada y con copia. Tampoco existe allí el modificador I para no distinguir mayúsculas, ni xargs -r. Si trabajas a menudo en macOS, brew install gnu-sed instala el de GNU como gsed.

  • ParaOrden
  • Ver dónde aparecegrep -rn 'texto' .
  • Solo los archivosgrep -rl 'texto' .
  • Texto literal, sin expresiones regularesgrep -F
  • Palabra completagrep -w
  • Probar el cambiosed 's/viejo/nuevo/g' archivo
  • Aplicarlo con copiased -i.bak 's/viejo/nuevo/g' archivo
  • En muchos archivosgrep -rlZ 'viejo' . | xargs -0 -r sed -i.bak 's/viejo/nuevo/g'
Antes de cambiar cientos de archivos, asegúrate de poder volver atrás: con la copia .bak, con una copia de la carpeta o, mejor todavía, con el proyecto en Git, donde git diff te enseña exactamente qué cambió y git checkout . lo deshace.

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.