Tratar con xml en Bash

El formato xml no es santo de mi devoción. Nunca he tenido buenas experiencias con él. No he sabido relacionarme de forma adecuada con este formato. Es mas, esperaba que a estas alturas del siglo XXI no tendría que estar relacionándome con él. Esperaba que a estas alturas de siglo, el formato json se habría impuesto. Sin embargo no ha sido así, y tengo que seguir lidiando con este formato, como tantas otras cosas, en las que he fallado con mi pronóstico. Hasta la fecha, siempre he tratado con xml utilizando Python y los módulos que tiene disponible para ello. Sin embargo, en este caso, he tenido que hacerlo directamente en Bash. Y esto es precisamente lo que voy a contarte en este artículo mi experiencia con xml en Bash.

Como de costumbre, no hay una solución única, sino que siempre tienes distintas maneras de afrontar el problema. En este caso, te contaré la solución que he adoptado y que he ido modificando y adaptando en función de las necesidades que han ido surgiendo.

Tratar xml en Bash

Tratar con xml en Bash

Inicialmente me decanté por xmllint para tratar con xml en Bash, sin embargo, no ha sido la única solución por la que he

Con el fin de hacer este artículo mas ameno e instructivo, voy a utilizar un archivo xml de ejemplo. Si tu también quieres hacer los mismos ejemplos, a modo de ejercicio, el archivo de ejemplo lo encontrarás en el enlace del menú de comida en xml.

Utilizando xmllint

La primera de las herramientas a la que recurrí es xmllint. Esta herramienta te permite parsear de forma sencilla archivos xml.

Formateando XML

Así, por ejemplo, lo primero que puedes hacer para tratar con un archivo xml en Bash, es formatearlo para que sea mas presentable al ojo humano. Para ello, simplemente tienes que ejecutar,

xmllint --format sample.xml

Esto te será de gran ayuda para el caso de que el contenido del archivo xml no esté correctamente formateado o que simplemente lo encuentres en una única línea.

Si además quisieras guardarlo formateado, simplemente tienes que redirigir la salida, tal y como te explico en el artículo sobre redirigir entrada y salida en Linux. Por ejemplo,

xmllint --format simple.xml > simple_con_formato.xml

Extraer información

Como te puedes imaginar, en mi caso, lo que mas me interesaba precisamente es extraer la información contenida en ese archivo xml. Para esto en particular, la opción mas interesante que te ofrece esta herramienta es -xpath. Así, si quieres obtener todos los precios del archivo de ejemplo tienes que ejecutar la siguiente instrucción,

xmllint --xpath "/breakfast_menu/food/price" simple.xml

Esto te devuelve algo como lo que te muestro a continuación,

<price>$5.95</price><price>$7.95</price><price>$8.95</price><price>$4.50</price><price>$6.95</price>

Evidentemente esto no es exactamente lo que tu necesitas… Por que tu quieres exactamente el contenido. La solución es recurrir a alguno de las soluciones que te cuento en el artículo filtros de texto. Yo, en particular he recurrido a sed. Así, la solución a lo que buscas podría ser algo como,

xmllint --xpath "/breakfast_menu/food/price" simple.xml | sed -E 's/<price>([^<]*)<\/price>/\1 /g'

Y definitivamente esto te devuelve el resultado que esperabas,

$5.95 $7.95 $8.95 $4.50 $6.95 

Un paso mas allá…

Y si quisieras imprimir para cada uno de los alimentos del menú, por ejemplo, el nombre y el precio. La solución podría ser algo como,

#/bin/bash
nombres=$(xmllint --xpath /breakfast_menu/food/name simple.xml | sed -E 's/<name>([^<]*)<\/name>/\1;/g')
precios=$(xmllint --xpath /breakfast_menu/food/price simple.xml | sed -E 's/<price>([^<]*)<\/price>/\1;/g')
IFS=';' read -ra nombres <<< "$nombres"
IFS=';' read -ra precios <<< $precios
for i in $(seq 0 $((${#nombres[@]} - 1)))
do
    echo "$((i+1)).- ${nombres[$i]} -> ${precios[$i]}"
done

Esto te arrojará un resultado como el que ves a continuación,

1.- Belgian Waffles -> $5.95
2.- Strawberry Belgian Waffles -> $7.95
3.- Berry-Berry Belgian Waffles -> $8.95
4.- French Toast -> $4.50
5.- Homestyle Breakfast -> $6.95

Como ves en este script he utilizado diferentes recursos que encontrarás en el tutorial sobre scripts en Bash.

Así para el uso de for, he utilizado lo descrito en el capítulo sobre bucles en Bash. Mientras que para tratar con arrays te puedes remitir al capítulo arrays en Bash.

Otras soluciones

Lo cierto es que no me terminaba de convencer la solución anterior y le estuve dando vueltas a otras solución. Y es que uno de los problemas que me encontraba eran los dichosos espacios en blanco. La solución la encontré en cut, herramienta sobre la que escribí en el capítulo de filtros: awk, grep, sed y cut del tutorial sobre el terminal.

Así, utilizando cut la solución queda mas limpia, o por lo menos esa es la impresión que yo me he llevado. Fíjate en el script,

#!/bin/bash
nombres=$(xmllint --xpath /breakfast_menu/food/name simple.xml | \
    sed -E 's/<name>([^<]*)<\/name>/\1;/g')
precios=$(xmllint --xpath /breakfast_menu/food/price simple.xml | \
    sed -E 's/<price>([^<]*)<\/price>/\1;/g')
items=$(echo ${nombres//[^;]} | wc -c)
for ((i=1;i<$items;i++))
do
    echo "$i.- $(echo $nombres | cut -d';' -f $i) -> \
$(echo $precios | cut -d';' -f $i)"
done

Aquí debes observar la forma en la que cuento los elementos, que es a partir de los separadores. En este caso he utilizado punto y coma. Para ello, lo que hago es quitar cualquier cosa que no sea punto y coma, y luego contarlo,

items=$(echo ${nombres//[^;]} | wc -c)

Y la otra parte interesante es el uso de cut. Donde he definido como delimitador, de nuevo, el punto y coma, he indico el campo que hay que extraer,

$(echo $nombres | cut -d';' -f $i)

Instalación

xmllint se encuentra en los repositorios oficiales de Ubuntu, dentro del paquete libxml2-utils. Así para instalarlo es tan sencillo como ejecutar la siguiente instrucción en tu terminal,

sudo apt install libxml2-utils

Otras opciones para tratar xml en Bash

Por supuesto que estas no son las únicas opciones, seguro que puedes encontrar muchas mas. Por ejemplo, también puedes utilizar xmlstarlet. Esta opción tiene algunas ventajas respecto a la solución anterior. Así, para el ejemplo que estás viendo,

xmlstarlet sel -t -v '/breakfast_menu/food/name/text()' simple.xml

Y la solución con esta otra herramienta podría tener un aspecto como el que ves a continuación,

#!/bin/bash
nombres=$(xmlstarlet sel -t -v '/breakfast_menu/food/name/text()' simple.xml)
precios=$(xmlstarlet sel -t -v '/breakfast_menu/food/price/text()' simple.xml)
items=$(echo "$nombres" | wc -l)
for ((i=1;i<=$items;i++))
do
    nombre=$(echo "$nombres" | head -n $i | tail -n +$i)
    precio=$(echo "$precios" | head -n $i | tail -n +$i)
    echo "$i.- $nombre -> $precio"
done

Esta otra herramienta también está en los repositorios oficiales de Ubuntu. Para instalarla solo tienes que ejecutar la siguiente instrucción en un terminal,

sudo apt install xmlstarlet

Conclusión

Como ves, y como de costumbre, tienes diferentes opciones para afrontar un determinado problema y llegar a la misma solución. Algunas de estas soluciones son mas rebuscadas o complejas y otras son mas limpias. En particular, mi recomendación, es que siempre te decantes por soluciones sencillas, porque al final, tendrás que mantener el código del script.


Imagen de James Osborne en Pixabay

1 comentario en “Tratar con xml en Bash

  1. SY
    Sys hace 1 año

    Sí, con Xmlstarlet queda más claro :-). Para continuar reduciendo el número de operaciones y la complejidad (y poder aplicar a casos parecidos), podemos substituir
    head -n $i | tail -n +$i
    por
    awk NR==$i
    (podríamos pensar que NR = “number of row”)
    ¡Gracias por el artículo!

Deja una respuesta

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