Poesía Binaria

Píldora Bash: Incluir archivos en nuestro script sin miedo a que nos cambien el directorio de ejecución

Es una buena práctica en cualquier lenguaje de programación, siempre que sea posible, el tener el código dividido en varios archivos. Esas divisiones harán que nuestro código esté mejor organizado en bloques o compartimentos diferenciados. Y, por supuesto, en nuestros shell scripts no vamos a ser menos.

De hecho, es muy común tener varios shell scripts en un directorio y todos ellos compartirán un código común. Y, por supuesto, está muy feo copiar y pegar ese código común en todos los archivos. Principalmente porque si necesitamos modificar ese código, debemos cambiarlo en todos los archivos, y eso puede llegar a ser muy pesado.

Por ello, vamos a hacer un pequeño programa, un hola mundo para la inclusión de scripts en Bash. Muy sencillo:
principal.sh:

1
2
3
4
5
#!/bin/bash

. funciones.sh

holamundo

functiones.sh:

1
2
3
4
function holamundo()
{
        echo "Hola Mundo!"
}

¿Por qué no ./funciones.sh?

Cuando utilizamos ./funciones.sh, o bash funciones.sh estaremos ejecutando el fichero y, las variables y funciones declaradas dentro de funciones.sh no podrán ser llamadas desde principal.sh. Más o menos nos interesa que el archivo principal contenga las declaraciones que hay en funciones.sh.

Muy sencillo, ahora la pega

Esto en teoría está muy bien, pero en la pŕactica se nos van a presentar algunos problemas:

No llamamos al script desde el directorio donde está creado

Sino que ponemos su ruta absoluta o relativa, o puede que lo tengamos en el $PATH. En mi caso yo tengo el script en /home/gaspy/scripts/principal.sh. Si estoy en mi $HOME puedo escribir:

scripts/principal.sh

o
/home/gaspy/scripts/principal.sh

o si tengo /home/gaspy/scripts en mi variable PATH, puedo estar en /tmp o en cualquier ruta de mi ordenador y ejecutar simplemente:
principal.sh

El problema aquí es que principal.sh siempre buscará functiones.sh en el directorio actual desde el que lo estemos ejecutando, y no siempre entrará en /home/gaspy/scripts/.

Una solución muy sencilla es escribir la ruta absoluta donde se encuentra el fichero al incluirlo (en principal.sh):

1
2
3
4
5
#!/bin/bash

. /home/gaspy/scripts/funciones.sh

holamundo

Así, en principio, no vamos a tener problema. Aunque si compartimos nuestros scripts con nuestros amigos, o los publicamos en github, estaríamos obligando a todo el mundo a tener el script en la misma ruta, y a lo mejor alguien quiere tenerlos en otro directorio, /home/pepe/scripts, /home/alice/scripts/, o /home/strawberry/local/bin.

Entonces tenemos que hacer que esta inclusión sea independiente de la ruta desde la que se llame el programa. Para ello tenemos varias formas. En principal.sh:

1
2
3
4
5
6
#!/bin/bash

SCRIPT_SOURCE="$(dirname "${BASH_SOURCE[0]}")"
. "$SCRIPT_SOURCE"/funciones.sh

holamundo

De esta manera, almacenamos en la variable SCRIPT_SOURCE el directorio donde está el script que hemos llamado, es decir, principal.sh y en función de la ruta de principal.sh incluimos funciones.sh . Por lo que, si nuestro funciones.sh está dentro de lib podríamos hacer:

1
2
SCRIPT_SOURCE="$(dirname "${BASH_SOURCE[0]}")"
. "$SCRIPT_SOURCE/lib/funciones.sh"

Son importantes las comillas dobles que vemos en el código, ya que si el directorio donde se encuentran los archivos tiene espacios (o, mejor dicho, tiene algún carácter del $IFS) podemos tener un problema al localizarlos.

Por ahora, todo parece funcionar bien. Tanto si llamamos desde ruta absoluta, relativa, incluimos el ejecutable en el $PATH o ejecutamos el archivo desde el mismo directorio. Pero, como somos muy enrevesados, vamos a dar una vuelta de tuerca más. Creando un enlace al archivo ejecutable en otro directorio y llamando a éste.

ln -s /home/gaspy/scripts/principal.sh /usr/local/bin
/usr/local/bin/principal.sh

Aunque bien podríamos haber ejecutado simplemente principal.sh porque /usr/local/bin/ suele estar en el $PATH y no tenemos por qué poner la ruta completa. El problema aquí es que el script que ejecutamos es /usr/local/bin/principal.sh y funciones.sh no está en /usr/local/bin sino en /home/gaspy/scripts/ por lo que debemos resolver la ruta del enlace para hallar el directorio real donde se encuentran los archivos. Para ello podemos hacer lo siguiente:

1
2
3
4
5
6
#!/bin/bash

SCRIPT_SOURCE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
. "
$SCRIPT_SOURCE/funciones.sh"

holamundo

En realidad, tener un enlace al ejecutable no es tan raro. Puede que tengamos una serie de scripts en un directorio y sólo queramos que el ejecutable esté en el $PATH o tal vez tengamos un directorio cuyos archivos se ejecutan cuando ocurre un evento, en lugar de tener los ejecutables en dicho directorio podemos tener enlaces a los mismos y si algo cambia no tenemos que tocar los archivos, sólo los enlaces.

Hasta el momento, igual que utilizamos ${BASH_SOURCE[0]}, podríamos utilizar $0, al final es el nombre del archivo que se ha ejecutado. Aunque $BASH_SOURCE tiene una funcionalidad extra, contiene todos los archivos que se han incluido para llegar al archivo actual. Si A, incluye B y B incluye C, desde A, BASH_SOURCE sólo contendrá un archivo, desde B, contendrá 2 y desde C contendrá 3.

Archivos que no deben ser ejecutados

En nuestro caso, funciones.sh no debe ser ejecutado directamente. Podemos quitarle permisos de ejecución, pero tampoco nos frenaría demasiado. El objetivo de no poder ejecutar el script es que pueda revelar información del sistema o pueda causar un mal funcionamiento. Así que estaría bien que el script se detuviera cuando intentemos ejecutarlo. Esto lo haremos con $BASH_SOURCE, contando cuántos elementos tiene. En este caso, si el array tiene sólo un elemento estaremos ante una ejecución directa del programa. Si $BASH_SOURCE tiene más elementos, estaremos incluyendo el archivo desde otro ejecutable principal. Con esto, incluso podremos restringir quién nos incluye.

Podemos hacer por ejemplo en funciones.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

if ((${#BASH_SOURCE[@]}==1)); then
        echo "Este archivo no puede ser ejecutado" >&2
        exit
fi

function holamundo()
{
        echo "Hola Mundo!"
}

echo "Inicializo funciones..."

Por lo tanto, lo primero que verifica el archivo es que no es una ejecución directa. Así el programa funcionará si llamamos a principal.sh (incluso mostrando el mensaje «Inicializo funciones…» y nos mostrará un error si llamamos a funciones.sh. También podemos hacer exit directamente, eso ya, a nuestro gusto.

Foto principal: Olga Gorbunova

También podría interesarte....