Poesía Binaria

9 Trucos para manejar cadenas de caracteres en BASH y no morir en el intento

Aunque este tema lo he tratado alguna vez un poco más de pasada o con algún ejemplo concreto, vamos a ver ejemplos concretos de cosas que podemos hacer con una cadena de caracteres en Bash, para mejorar nuestros scripts, y sobre todo, para tener una pequeña documentación para mí, que siempre que necesito algo parecido me tiro un rato buscando…
El caso es que en versiones más o menos nuevas de BASH (más o menos desde hace 6 años o así), podemos usar muchos de estos trucos y no tenemos necesidad de utilizar programas externos (como tr, awk, sed y otros) en muchos casos, por lo que la ejecución se realizará mucho más rápida.

1 – Reemplazar subcadenas

Siempre podemos usar sed o awk, pero, si lo hacemos de forma nativa y nuestras necesidades no son muy grandes, podemos hacerlo desde bash, para ello podemos usar:

1
echo ${variable/subcadena/reemplazo}

bueno, mejor con un ejemplo

1
2
tigres="Un tigre, dos tigres, tres tigres"
echo "${tigres/tigre/gato}"

Aquí sustituiremos la palabra tigre por gato dentro de $tigres:

Un gato, dos tigres, tres tigres

pero claro, lo suyo es que se reemplacen todas, para ello, podemos usar:

1
2
tigres="Un tigre, dos tigres, tres tigres"
echo "${tigres//tigre/gato}"

que da como salida lo siguiente,

Un gato, dos gatos, tres gatos

No tenemos tanta potencia como con sed, por ejemplo, no podemos utilizar expresiones regulares, aunque sí que podemos usar * (lo que sea y los caracteres que sean) y ? (el carácter que sea) como comodines: «t*e» podrá ser tigre, torre, tarde, tirante, toreo… o «ti??e» podrá ser tigre, tilde, tinte, tintero…

También podemos utilizar corchetes [] para especificar rangos de caracteres como [a-z] de esta forma:

1
2
tigres="Un tigre, dos tigres, tres tigres"
echo "${tigres//[a-z]/*}"

** *****, *** ******, **** ******

o

1
2
tigres="Un tigre, dos tigres, tres tigres"
echo "${tigres//[aeiou]/*}"

Un t*gr*, d*s t*gr*s, tr*s t*gr*s

Tenemos dos modificadores más para reemplazar cadenas al principio y al final y sólo si encontramos la subcadena en esas posiciones. Por ejemplo:

1
2
bytes="123 bytes"
echo ${bytes/#[0-9]* /muchos }

muchos bytes

reemplazo que no se producirá si la cadena es «tengo 123 bytes por ejemplo», sólo funcionará si encuentra un número, para hacerlo mejor podemos utilizar, por ejemplo:

1
2
bytes="tengo 123 bytes"
echo ${bytes/#*([0-9])/muchos}

Para lo que he utilizado extglob, una forma de extender patrones que nos puede resultar muy útil, *([0-9]) concordará con ninguno o más números entre el 0 y el 9, podemos utilizarlos con letras, por ejemplo, o incluir algún símbolo si queremos que cumpla la condición. como caracteres comodín podemos utilizar ? (que coincidan cero o uno) , + (que coincidan uno o más entre otros (nos podemos extender en otro post con ejemplos de esto.

Por último, podemos hacer que se reemplace una coincidencia basándonos en la terminación de la cadena:

1
2
bytes="tengo 123 bytes"
echo ${bytes/%bytes/megas}

tengo 123 megas

2 – Extraer la ruta de un archivo

Tenemos el archivo, /usr/share/icons/hicolor/64×64/mimetypes/application-wireshark-doc.png, por coger uno. Y queremos extraer la ruta del archivo: /usr/share/icons/hicolor/64×64/mimetypes/, ¿para qué? tal vez sea un directorio donde tenemos más cosas que necesitemos, o queremos crear un archivo de log en el mismo directorio. Para sacarlo, debemos hacer:

1
2
myfile="/usr/share/icons/hicolor/64x64/mimetypes/application-wireshark-doc.png"
echo "${myfile%/*}"

lo cual nos devolverá:

/usr/share/icons/hicolor/64×64/mimetypes

3 – Extraer el archivo sin la ruta

Tenemos el mismo nombre de archivo /usr/share/icons/hicolor/64×64/mimetypes/application-wireshark-doc.png. Y desde nuestro script, necesitáis extraer el nombre de archivo, eliminando la ruta /usr/share/icons/hicolor/64×64/mimetypes/.
¿ Para qué puede servir ? Por ejemplo, si nuestro script se dedica a convertir archivos, o a copiar o mover archivos de un sitio a otro, copias de seguridad, enviarlos por la red… en esos casos puede interesarnos mantener el nombre original, pero la ruta no va a ser la misma.
Podemos hacerlo de la siguiente manera:

1
2
myfile="/usr/share/icons/hicolor/64x64/mimetypes/application-wireshark-doc.png"
echo "${myfile##*/}"

lo cual nos devolverá:

application-wireshark-doc.png

4 – ¡Ey! Un momento !! %% y ## son cosas especiales

Pues sí, son caracteres de control que podemos usar en bash para extraer partes de una cadena según un patrón, en realidad el funcionamiento interno lo veo poco intuitivo, y creo que se entiende mejor de esta forma), así:

${variable##patron}

Elimina todo lo que encuentra desde el principio de la cadena hasta la última aparición del patrón. Si el patrón es «*/» (o lo que sea, y luego una /), como es el caso de las rutas, eliminará todo hasta la última barra quedándonos con lo que hay detrás (el nombre de archivo).

¿Qué pasa si en nuestro patrón hay asteriscos? Por ejemplo si tenemos:

1
test="tengo*asteriscos*a*cascoporro"

Para extraer «cascoporro» deberíamos hacer:

1
echo "${test##*\*}"

En efecto, escapamos el asterisco final, que es el que queremos que no se interprete.

${variable#patron}

Es lo mismo que el apartado anterior, pero eliminará todo desde el principio de la cadena hasta la primera aparición del patrón, en el caso de las rutas, nos quitaría el texto desde el principio hasta la primera /, en el caso de los asteriscos, quitaría «tengo*»

${variable%patron}

Ahora vamos al revés, en lugar de empezar desde el principio, empezaremos desde el final, por lo tanto eliminaremos la otra parte de la cadena. En el caso de la extracción de la ruta, leyendo hacia atrás, eliminamos la cadena desde el final hasta la primera aparición del patrón (hacia atrás, si leemos desde la izquierda, será la última).

${variable%%patron}

Es lo mismo que el anterior, pero eliminaremos hasta la última aparición del patrón (desde el final) o la primera, si miramos desde la izquierda.

5 – Extraer el nombre de archivo sin la extensión

Ayudándonos de %. una vez eliminada la ruta del archivo, debemos eliminar la extensión, nos puede servir para cambiar la extensión de un archivo (si estamos convirtiendo) o generar un archivo .log derivado del mismo nombre, etc.

1
2
3
myfile="/usr/share/icons/hicolor/64x64/mimetypes/application-wireshark-doc.png"
filename="${myfile##*/}"
echo ${filename%.*}

Lo que nos devolverá

application-wireshark-doc

Pero seamos malos, si el archivo fuera www/projects/poesiabinaria/js/jquery.min.js

1
2
3
myfile="www/projects/poesiabinaria/js/jquery.min.js"
filename="${myfile##*/}"
echo ${filename%.*}

El resultado será

jquery.min

Si quisiéramos extraer .min.js, deberíamos poner echo ${filename%%.*}

6 – Sacar sólo la extensión del archivo

Si, en este caso, dependiendo de la extensión del archivo tenemos que variar el funcionamiento de nuestro script, podemos hacer lo siguiente:

1
2
myfile="www/projects/poesiabinaria/js/jquery.min.js"
echo ${filename##*.}

En este caso, no necesitamos extraer la ruta primero, porque eliminaremos el contenido desde delante. Obtendremos:

js

Si nos quisiéramos quedar con min.js, sí que es recomendable eliminar la ruta porque algún directorio puede tener puntos y nos devolvería algo extraño:

1
2
3
myfile="www/projects/poesiabinaria/js/jquery.min.js"
filename="${myfile##*/}"
echo ${filename#*.}

y obtendremos:

min.js

7 – Contar caracteres de una cadena

Basta de usar:

1
2
cadena="murciélago"
echo $cadena | wc -l

Además, porque puede dar un fallo con los caracteres especiales (UTF-8 frente a ISO8859-1 y esas cosas), lo que puede que cuente un número diferente de letras, al contar los bytes de la palabra. Por otro lado, es mucho más eficiente esto:

1
2
cadena="murciélago"
echo ${#cadena}

Sencillo, ¿no?

8 – Recortar cadenas

Un ejemplo vale más que 1000 explicaciones:

1
2
bienvenido="Bienvenido al Blog Poesía Binaria, Programación, Tecnología y Software libre"
echo ${bienvenido:18:15}

Éste empezará en la posición 18 y contará 15 caracteres.

Poesía Binaria

Ahora, podemos jugar con los valores:

1
2
bienvenido="Bienvenido al Blog Poesía Binaria, Programación, Tecnología y Software libre"
echo ${bienvenido:18}

Éste recortará desde la posición 18 hasta el final.

Poesía Binaria, Programación, Tecnología y Software libre

o

1
2
bienvenido="Bienvenido al Blog Poesía Binaria, Programación, Tecnología y Software libre"
echo ${bienvenido:18:-16}

Éste recortará empezando por el final.

Poesía Binaria, Programación, Tecnología

9 – Valor por defecto

Es una buena técnica inicializar todas las variables y que todas tengan un valor conocido. También es muy útil que si una variable no se ha definido con anterioridad, imaginad que se define gracias a un valor de usuario que no se ha especificado, adopte un valor por defecto. De esta forma, imaginad la variable host, si no se especifica:

1
echo ${host:=localhost}

Sólo escribiendo esto, la variable host se define y su valor será «localhost», pero, si por ejemplo, ya se encontraba definida:

1
2
host="totaki.com"
echo ${host:=localhost}

Aquí obtenemos directamente, «totaki.com» y la variable host no se ve modificada. Esto nos puede ahorrar unos cuantos if 🙂
Si, por ejemplo, no queremos asignar a la variable host el valor por defecto, podemos utilizar:

1
echo ${host:-localhost}

de este modo, visualizaremos el valor por defecto si la variable no está definida, pero no le asignaremos el nuevo valor a la variable.

¿ Tienes algún truco o ejemplo que suelas usar con cadenas en bash ?

También podría interesarte....