Con el Terminal: Uso de expresiones regulares II: Reemplazos

En mi artículo anterior os he contado a un nivel básico cómo funcionan cada uno de los caracteres especiales más usados de las expresiones regulares. Con esas expresiones regulares es posible hacer búsquedas complejas en ficheros de texto o en la salida de otros comandos. En este artículo voy a explicar como usar el comando sed para buscar y reemplazar texto de una forma mucho más potente que simplemente cambiar un texto por otro.

Un poco más sobre el comando grep

Antes de empezar a hablar sobre sed, me gustaría comentar algo más sobre el comando grep para completar un poco lo explicado en el anterior artículo. Todo lo que voy a decir será relevante para este también. Más adelante veremos la relación que hay entre esto y las búsquedas.

Combinando expresiones regulares

Muchos de los caracteres especiales de los que os he hablado en el artículo anterior se pueden combinar, no sólo con otros caracteres, sino con expresiones regulares enteras. La forma de hacer esto es usar paréntesis para formar una subexpresión. Vamos a ver un ejemplo de esto. Empecemos descargando un texto que nos sirva para hacer pruebas. Se trata de una lista de frases. Para eso vamos a usar el siguiente comando:

curl http://artigoo.com/lista-de-frases-comparativas-comicas 2>/dev/null | sed -n 's/.*\(.*\.\)<\/p>/\1/gp' > frases

 Esto os dejara en el directorio donde lo lanzéis un fichero con nombre «frases». Podéis abrirlo para echarle un vistazo y reíros un poco. 🙂

Ahora vamos a suponer que queremos  encontrar las frases que tengan exactamente 6 palabras. La dificultad está en formar una expresión regular que empareje con cada palabra. Una palabra es una secuencia de letras ya sean mayúsculas o minúsculas, lo que sería algo así como '[a-zA-Z]+', pero también hay que especificar que estas letras tienen que estar separadas por otros caracteres que no sean letras, o sea que sería algo como '[a-zA-Z]+[^a-zA-Z]+'. Recordemos: el «^» como primer carácter dentro de los corchetes indica que queremos emparejar con caracteres que no están en los rangos y el «+» indica 1 o más caracteres.

Ya tenemos una expresión regular que puede emparejar con una palabra. Para emparejarla con 6, habrá que repetirla 6 veces. Para eso usábamos las llaves, pero no sirve poner '[a-zA-Z]+[^a-zA-Z]+{6}', porque el 6 repetiría la última parte de la expresión regular y lo que queremos es repetirla toda, así que lo que hay que poner es esto: '([a-zA-Z]+[^a-zA-Z]+){6}'. Con los paréntesis formamos una subexpresión y con las llaves la repetimos 6 veces. Ya sólo falta añadir un «^» delante y un «$» detrás para emparejar con la linea entera. El comando es el siguiente:

grep -E '^([a-zA-Z]+[^a-zA-Z]+){6}$' frases

Y el resultado es justo el que queríamos:

Está más cantado que la Macarena.
Estás más acabado que Luis Aguilé.
Tienes menos cultura que una piedra.
Sabes más idiomas que Cañita Brava.
Tiene más arrugas que Tutan Khamón.
Sabes menos que Rambo de puericultura.

Fijaros en que ponemos el parámetro -E porque queremos usar expresiones regulares extendidas para que funcione el «+». Si usásemos las básicas, habría que escapar los paréntesis y las llaves.

Referencias hacia atrás o backreferences

Si tenéis instalado algún corrector ortográfico, probablemente tendréis una lista de palabras en /usr/share/dict/words. Si no es así, podéis instalarla en arch con:

sudo pacman -S words

O en debian con:

sudo aptitude install dictionaries-common

Si queréis podéis echarle un vistazo al fichero para ver qué palabras tiene. En realidad es un enlace al fichero de palabras del idioma en el que esté vuestra distro. Se pueden tener varios ficheros de palabras instalados a la vez.

Vamos a usar ese fichero. Resulta que tenemos mucha curiosidad por saber todos los palíndromos de siete letras que hay. Para el que no lo sepa: Un palíndromo es una palabra capicúa, o sea, que se puede leer igual de izquierda a derecha que de derecha a izquierda. Probemos el siguiente comando:

grep '^\(.\)\(.\)\(.\).\3\2\1$' /usr/share/dict/words

Tiene un aspecto un poco extraño, ¿verdad? Si lo probamos, el resultado dependerá del idioma de vuestra distro y de las palabras que haya en vuestra lista, pero en mi caso, con el idioma español, el resultado es este:

anilina
recocer
rodador

Vamos a ver cómo funciona esta expresión regular.

Aparte del «^» y el «$», que ya sabemos para qué sirve, lo primero que vemos a la izquierda son tres grupos de puntos encerrados entre paréntesis. Que no os confundan las barras que hay delante de cada paréntesis. Son para escapar los paréntesis porque estamos usando expresiones regulares básicas, pero no tienen ningún otro significado. Lo importante es que estamos pidiendo con los puntos tres caracteres cualesquiera, pero cada uno de esos puntos están encerrados entre paréntesis. Esto sirve para que guarde los caracteres que encajan con esos puntos de manera que se pueda volver a hacer referencia a ellos desde la expresión regular. Este es otro uso de los paréntesis que será muy útil más adelante para hacer reemplazos.

Aquí es donde vienen los tres números que hay a continuación con la barra delante. En este caso, la barra sí es importante. Sirve para indicar que el número que hay a continuación es una backreference y está haciendo referencia a uno de los paréntesis anteriores. Por ejemplo: \1 hace referencia al primer paréntesis, \2 al segundo y así sucesivamente.

O sea que con la expresión regular que hemos puesto, lo que buscamos son todas las palabras que empiecen por cuatro letras cualesquiera y luego tengan una letra que sea igual que la tercera, otra que sea igual que la segunda y otra que sea igual que la primera. El resultado son los palíndromos de siete letras que estén en la lista de palabras. Tal como queríamos.

Si estuviéramos usando expresiones regulares extendidas, no habría que escapar los paréntesis, pero con expresiones regulares extendidas no funcionan las backreferences en todos los programas porque no están estandarizadas. Sin embargo, con grep funcionan, o sea que esa puede ser otra forma de hacer lo mismo. Podéis probarlo si queréis.

Expresiones de reemplazo: El comando sed

Además de hacer búsquedas, una de las mejores utilidades de las expresiones regulares es reemplazar textos complejos. Para ello, una forma de hacerlo es con el comando sed. La potencia del comando sed va mucho más allá de reemplazar textos, pero aquí voy a utilizarlo para eso. La sintaxis que voy a usar con este comando es la siguiente:

sed [-r] 's/REGEX/REPL/g' FICHERO

O también:

COMANDO | sed [-r] 's/REGEX/REPL/g'

Donde REGEX será la expresión regular de búsqueda y REPL la de reemplazo. Tened en cuenta que este comando no reemplaza realmente nada en el fichero que le indiquemos, sino que lo que hace es mostrarnos el resultado del reemplazo en la terminal, así que no os asustéis por los comandos que voy a poner a continuación. Ninguno de ellos va a modificar ningún fichero de vuestro sistema.

Empecemos con un ejemplo sencillo. Todos tenemos en el directorio /etc varios ficheros de configuración que, normalmente, tienen comentarios que empiezan por «#». Supongamos que queremos ver uno de estos ficheros sin los comentarios. Por ejemplo, voy a hacerlo con el fstab. Podéis probar con el que queráis.

sed 's/#.*//g' /etc/fstab

No voy a poner aquí el resultado del comando porque depende de lo que tengáis en vuestro fstab, pero si comparáis la salida del comando con el contenido del fichero veréis que todos los comentarios han desaparecido.

En este comando la expresión de búsqueda es «#.*«, o sea un «#» seguido de cualquier número de caracteres, o sea, los comentarios. Y la expresión de reemplazo, si os fijáis en las dos barras seguidas, veréis que no hay ninguna, así que lo que está haciendo es reemplazar los comentarios por nada, o sea, borrarlos. Más sencillo imposible.

Ahora vamos a hacer lo contrario. Supongamos que lo que queremos es comentar todas las líneas del fichero. Probemos así:

sed 's/^/# /g' /etc/fstab

Veréis que, en la salida del comando, todas las líneas empiezan por una almohadilla y un espacio en blanco. Lo que hemos hecho es reemplazar los inicios de línea por «# «. Este también es un ejemplo bastante simple en el que el texto por el que se reemplaza es siempre el mismo, pero ahora vamos a complicarlo un poco más.

La gracia de los reemplazos está en que en la expresión de reemplazo se pueden utilizar backreferences como las que os he contado antes. Volvamos al fichero de frases que nos hemos descargado al principio del artículo. Vamos a meter entre paréntesis todas las letras mayúsculas que haya, pero lo haremos con un comando:

sed 's/\([A-Z]\)/(\1)/g' frases

Lo que tenemos aquí es una backreference en la expresión de reemplazo que hace referencia al paréntesis que hay en la expresión de búsqueda. Los paréntesis que hay en la expresión de reemplazo son paréntesis normales. En la expresión de reemplazo no tienen ningún significado especial, se ponen tal cual. El resultado es que todas las letras mayúsculas se reemplazan por esa misma letra, sea cual sea, con paréntesis alrededor.

Hay otro carácter que también se puede usar en la expresión de reemplazo, es «&» y se reemplaza por todo el texto emparejado por la expresión de búsqueda. Un ejemplo con esto podría ser meter todas las frases del fichero entre comillas. Esto se puede conseguir con este comando:

sed 's/.*/"&"/g' frases

El funcionamiento de este comando es muy similar al anterior, sólo que ahora lo que reemplazamos es la línea entera por esa misma linea con comillas alrededor. Como estamos usando «&» no hace falta poner paréntesis.

Algunos comandos útiles con expresiones regulares

A continuación os voy a dejar unos cuantos comandos que me parecen útiles o curiosos y que utilizan expresiones regulares. Con estos comandos se ve mucho mejor la utilidad de las expresiones regulares que con los ejemplos que os he puesto hasta ahora, pero me parecía importante explicar algo del funcionamiento de las expresiones regulares para poder entender estas.

  • Mostrar las secciones de una página del manual:

man bash | grep '^[A-Z][A-Z ]*$'

Por supuesto, podéis cambiar el comando bash por el que queráis. Y luego desde man, podéis ir directamente a la sección que os interesa usando, como no, una expresión regular. Pulsáis «/» para empezar a buscar y escribís «^ALIASES$» para ir a la sección ALIASES, por ejemplo. Creo que este es el primer uso que empece a hacer de las expresiones regulares hace ya unos cuantos años. Moverse por algunas páginas del manual es casi imposible si no se usa algún truco como este.

  • Mostrar los nombres de todos los usuarios de la máquina incluidos los especiales:

sed 's/\([^:]*\).*/\1/' /etc/passwd

  • Mostrar los nombres de los usuarios, pero sólo los que tienen shell:

grep -vE '(/false|/nologin)$' /etc/passwd | sed 's/\([^:]*\).*/\1/g'

Realmente se puede hacer con una sola expresión regular, pero la forma de hacerlo va más allá de lo que os he contado en estos artículos, así que lo he hecho combinando dos comandos.

  • Insertar una coma delante de las tres últimas cifras de todos los números que haya en el fichero numbers:

sed 's/\(^\|[^0-9.]\)\([0-9]\+\)\([0-9]\{3\}\)/\1\2,\3/g' numbers

Sólo funciona con números de hasta 6 dígitos, pero se puede lanzar más de una vez para colocar separadores en los demás grupos de tres cifras.

  •  Extraer todas las direcciones de email de un fichero:

grep -E '\<[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}\>' FICHERO

  • Separar el día, mes y año de todas las fechas que aparezcan en un fichero:

sed -r 's/([0-9]{2})[/-]([0-9]{2})[/-]([0-9]{4})/Día: \1, Mes: \2, Año: \3/g' FICHERO

  • Averiguar nuestra IP local:

/sbin/ifconfig | grep 'inet .*broadcast' | sed -r 's/[^0-9]*(([0-9]+\.){3}[0-9]+).*/\1/g'

Esto también se puede hacer con un sólo comando sed, pero mejor lo separo en un grep y un sed para simplificarlo.

Algunas direcciones útiles

Os dejo a continuación algunas direcciones que pueden ser útiles relacionadas con las expresiones regulares:

  • Regular expression library: Se trata de una biblioteca de expresiones regulares en la que podéis buscar expresiones regulares relacionadas con el tema que os interese. Para buscar direcciones web, DNI o lo que sea.
  • RegExr: Un comprobador online de expresiones regulares. Permite introducir un texto y aplicarle una expresión regular ya sea de búsqueda o de reemplazo. Da información sobre la expresión regular y tiene algunas opciones para cambiar su comportamiento.
  • Regular Expressions Tester: Es un addon para firefox que permite comprobar expresiones regulares desde el navegador.

Conclusión

Por ahora eso es todo. Las expresiones regulares son complejas pero útiles. Lleva tiempo aprenderlas, pero si sois como yo, jugar con ellas os parecerá divertido y, poco a poco iréis dominándolas. Es todo un mundo. Habría mucho que decir todavía, sobre cuantificadores lazy, expresiones regulares estilo PERL, multilínea, etc. Y luego cada programa tiene sus características y sus variantes, así que el mejor consejo que os puedo dar es mirar siempre la documentación del programa que estéis usando cada vez que tengáis que escribir una expresión regular en un programa nuevo.

¡Eh! …¡EH! …¡DESPERTAD! …¿QUE HACÉIS TODOS DURMIENDO? 🙂

Fuentes

Algunas de las ideas y ejemplos para las expresiones regulares de este artículo las he tomado de aquí:


15 comentarios, deja el tuyo

Deja tu comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*

*

  1. Responsable de los datos: Miguel Ángel Gatón
  2. Finalidad de los datos: Controlar el SPAM, gestión de comentarios.
  3. Legitimación: Tu consentimiento
  4. Comunicación de los datos: No se comunicarán los datos a terceros salvo por obligación legal.
  5. Almacenamiento de los datos: Base de datos alojada en Occentus Networks (UE)
  6. Derechos: En cualquier momento puedes limitar, recuperar y borrar tu información.

  1.   elav dijo

    Magistral!!!

    1.    hexborg dijo

      No es para tanto, pero muchas gracias. Espero que a la gente le guste. 🙂

      1.    oscar dijo

        Me gusta ja!

        1.    hexborg dijo

          Entonces habré hecho algo bien. ¡Jajajaja!! 🙂

          Muchas gracias por tu comentario.

          1.    Blaire Pascal dijo

            Joder sigue escribiendo tío, sigue así.

          2.    hexborg dijo

            @Blaire Pascal: Comentarios como el tuyo animan a ello. 🙂 ¡Muchas gracias!!

      2.    Citux dijo

        A mi también me gustó…gracias 🙂

        1.    hexborg dijo

          Gracias a ti por comentar. Espero escribir unos cuantos más. 🙂

  2.   mariano dijo

    Tus posts son fantásticos, se aprende mucho, mejor dicho, se aprende a realizar las tareas de manera elegante y eficiente.

    ¿Has pensado en recopilar todos tus posts de shell script? Ordenaditos en un pdf serían un gran manual.

    ¡Ánimos y muchas gracias!

    1.    hexborg dijo

      ¡Muchas gracias!! No es mala idea. De momento sólo son dos, pero más adelante pensaré en ello. 🙂

  3.   Kiyov dijo

    muy buen articulo, 5+.

    1.    hexborg dijo

      Gracias. Me alegro de que te guste. 🙂

  4.   sebastian dijo

    Excelente! Necesito cambiar la siguiente expresión y no se como hacerlo:
    192.168.0.138/Server por 192.168.0.111/datos
    El problema radica en el símbolo «/».
    Estoy usando el comando:
    find . -name «*.txt» -exec sed -i ‘s/TEXTO1/TEXTO2/g’ {} \;
    Que sirve para realizar este tipo de tareas remisivamente, pero no logro…
    Alguien sabe como debo hacerlo?
    Abrazo!
    Seba

    1.    hexborg dijo

      Lo que hay que hacer es escapar el caracter así:

      find . -name «*.txt» -exec sed -i ‘s/\/Server/\/datos/g’ {} \;

      También puedes usar otro separador en sed. No tiene porque ser una barra. Sed permite usar cualquier caracter. Por ejemplo, esto sería más claro:

      find . -name «*.txt» -exec sed -i ‘s|/Server|/datos|g’ {} \;

      Y si vas a copiar y pegar los comandos desde este comentario ojo con las comillas, que wordpress las cambia por las tipográficas. 🙂

      Saludos.

  5.   sebastian dijo

    Excelente!!!!
    Andaba buscando esta solución hacía tiempo.
    Aquí dejo el comando completo que he utilizado

    find . -name «*.txt» -exec sed -i ‘s|192\.168\.0\.238\/Server|192\.168\.0\.111\/datos|g’ {} \;

    La ventaja de este comando es que cambia todos los ficheros .txt (o la extensión que desees) recursivamente… Hay que tener mucho cuidado!
    Pero es muy útil !!!

    Bueno, gracias por todo y mil felicitaciones al grupo entero.
    Siempre los leo desde el correo!
    Abrazos
    Seba