1. Cargar un archivo con source
Como vimos en el tema 2, source archivo, o su forma abreviada . archivo, lee un archivo y ejecuta sus líneas en la shell actual, sin crear un proceso. Si el archivo solo contiene definiciones de funciones y variables, el efecto es que esas funciones y variables quedan disponibles en el script que lo cargó, como si estuvieran escritas en él.
# comun.sh
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
# mi-script
#!/usr/bin/env bash
source ./comun.sh
error "algo salió mal" # la función viene de comun.sh
Dentro de la biblioteca, $0 sigue siendo el script que la cargó, porque no hay un proceso nuevo: por eso error muestra el nombre de mi-script, que es lo correcto. Y todo lo que la biblioteca define queda en el mismo espacio que el script: sus funciones, sus variables globales y cualquier opción que active, como set -e.
El ejemplo tiene un problema, sin embargo: ./comun.sh es una ruta relativa al directorio actual, no al del script. Funciona si se ejecuta el script desde su propio directorio, y falla desde cualquier otro.
2. Encontrar la biblioteca
La forma robusta es construir la ruta de la biblioteca a partir de la ubicación del propio script. Bash la ofrece en el array BASH_SOURCE: ${BASH_SOURCE[0]} es la ruta del archivo que contiene la línea que se está ejecutando.
| Variable | En el script | Dentro de una biblioteca cargada |
|---|---|---|
$0 | La ruta con que se invocó el script. | La misma: la del script. |
${BASH_SOURCE[0]} | La ruta del script. | La ruta de la biblioteca. |
Para cargar una biblioteca que está junto al script, o en un directorio relativo a él, se toma el directorio de esa ruta:
readonly DIR_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR_SCRIPT/../lib/comun.sh"
El cd seguido de pwd convierte la ruta en absoluta: si el script se invocó como ../herramientas/bin/script, dirname daría una ruta relativa, válida solo desde el directorio de ese momento. Con la ruta absoluta, el script puede cambiar de directorio después y seguir encontrando sus archivos. Se usa BASH_SOURCE y no $0 porque funciona también cuando el propio script es cargado con source por otro, como en las pruebas.
3. Scripts instalados mediante un enlace
En el tema 2 propusimos instalar un script en ~/bin mediante un enlace simbólico a su ubicación en el proyecto. Con bibliotecas, eso tiene una consecuencia: BASH_SOURCE contiene la ruta del enlace, ~/bin/script, y la biblioteca no está en ~/bin/../lib sino en el proyecto.
readlink -f resuelve los enlaces y devuelve la ruta real del archivo. Agregándolo, el script encuentra sus bibliotecas lo invoquen como lo invoquen:
readonly DIR_BASE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
source "$DIR_BASE/lib/comun.sh"
| Paso | Con ~/bin/limpiar → ~/proyectos/herramientas/bin/limpiar |
|---|---|
${BASH_SOURCE[0]} | /home/ana/bin/limpiar |
readlink -f | /home/ana/proyectos/herramientas/bin/limpiar |
dirname | /home/ana/proyectos/herramientas/bin |
/.., cd y pwd | /home/ana/proyectos/herramientas |
readlink -f es de GNU; en macOS existe desde la versión 12.3, y antes había que usar realpath o un equivalente. Es una línea larga, pero se escribe una vez al comienzo de cada script y no hay que volver a pensar en ella.
4. Qué va en una biblioteca, y qué no
Una biblioteca se ejecuta dentro del script que la carga, de modo que todo lo que haga, lo hace en él. Por eso debe limitarse a definir, sin actuar:
| Sí | No |
|---|---|
| Definiciones de funciones. | Código suelto que hace algo al cargarse: crear archivos, mostrar mensajes, cambiar de directorio. |
| Constantes propias, de solo lectura y con prefijo. | set -euo pipefail u otras opciones: esa decisión es del script. |
Valores por defecto que el script pueda cambiar antes de cargarla: : "${LOG_NIVEL:=INFO}". | exit: terminaría el script que la cargó. |
| Un comentario al comienzo que diga qué contiene y cómo se usa. | Una línea shebang ni permiso de ejecución: no es un programa. |
Las funciones de una biblioteca, en cambio, sí pueden terminar el script, como morir, porque se las llama a propósito. Lo que no debe hacer es actuar por el solo hecho de ser cargada.
5. Cargar una sola vez
En un proyecto con varias bibliotecas, unas dependen de otras: archivos.sh puede necesitar morir de comun.sh, y el script carga las dos. comun.sh terminaría cargándose dos veces. En la mayoría de los casos no pasa nada grave, pero si la biblioteca declara constantes con readonly, la segunda carga falla. La costumbre es poner una guarda al comienzo de cada biblioteca:
# comun.sh — funciones generales para los scripts del proyecto.
# Se carga con «source»; no ejecuta nada por sí misma.
[[ -n "${_COMUN_SH:-}" ]] && return 0
_COMUN_SH=1
…
La primera vez, la variable no existe, la condición es falsa y la biblioteca se carga, dejando la variable definida. La segunda vez, la variable ya existe y return termina la carga en ese punto. return es válido en un archivo cargado con source, y termina solo la carga del archivo, no el script. El valor por defecto :- evita el error de set -u en la primera carga.
6. Nombres que no choquen
Como vimos en el tema 18, las funciones y las variables globales comparten un único espacio de nombres. Una biblioteca que define limpiar pisa, sin aviso, una función del mismo nombre en el script, o en otra biblioteca. En un proyecto propio y pequeño, con funciones bien nombradas, el riesgo es bajo. Cuando la biblioteca crece o se comparte, conviene usar un prefijo común:
# registro.sh — funciones de registro; todas empiezan con «log_».
: "${LOG_NIVEL:=INFO}" # el script puede fijarlo antes de cargarla
log_info() { _log_escribir INFO "$@"; }
log_aviso() { _log_escribir AVISO "$@"; }
log_error() { _log_escribir ERROR "$@"; }
_log_escribir() { # interna: el guion bajo inicial lo indica
…
}
Las funciones que la biblioteca usa internamente, y que los scripts no deberían llamar, llevan además un guion bajo inicial. Bash no impide llamarlas, pero el guion bajo comunica la intención. Lo mismo vale para sus variables globales: LOG_NIVEL es configuración pública; _COMUN_SH, un detalle interno.
7. ¿Se ejecuta o se carga?
A veces conviene que un mismo archivo pueda usarse de las dos formas: ejecutado, como un comando, y cargado, como biblioteca, por ejemplo para probar sus funciones por separado. Comparando BASH_SOURCE con $0 se distingue el caso: si son iguales, el archivo se está ejecutando; si no, alguien lo cargó con source.
main() {
…
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
Ejecutado, el script funciona normalmente. Cargado con source desde una prueba, define sus funciones sin ejecutar main, y la prueba puede llamar a cada función con distintos datos y comprobar el resultado. Es la base de las herramientas de pruebas para Bash, como bats, que se usan en proyectos de scripts grandes.
8. Organizar un proyecto
Cuando los scripts de una misma área se multiplican, conviene reunirlos en un proyecto con una estructura reconocible. Esta es una organización habitual:
herramientas/
├── bin/ # los scripts ejecutables, sin extensión
│ ├── archivar-descargas
│ ├── respaldar
│ └── verificar-sitios
├── lib/ # las bibliotecas, con extensión .sh
│ ├── comun.sh
│ ├── archivos.sh
│ └── registro.sh
├── conf/ # archivos de configuración de ejemplo
│ └── servicios.conf.ejemplo
├── pruebas/ # pruebas de las funciones
└── LEEME.md # qué es, cómo se instala, qué hace cada script
Los nombres siguen las convenciones del tema 2: los ejecutables sin extensión, porque son comandos; las bibliotecas con .sh, porque se cargan con source. Con el proyecto en un repositorio Git, cada cambio queda registrado, se puede volver atrás y se puede instalar el mismo conjunto de herramientas en varias máquinas con un git clone.
9. Instalar el proyecto
Hay dos formas de dejar los scripts disponibles como comandos, y las dos funcionan con la línea de readlink -f de la sección 3:
| Forma | Cómo | Cuándo |
|---|---|---|
Agregar bin al PATH | export PATH="$HOME/proyectos/herramientas/bin:$PATH" en ~/.bashrc | Para uso personal: todos los scripts del proyecto quedan disponibles. |
Enlaces en ~/bin | ln -s ~/proyectos/herramientas/bin/respaldar ~/bin/ | Para elegir qué scripts instalar. |
Para instalar en el sistema, para todos los usuarios, la convención es copiar las bibliotecas a /usr/local/lib/herramientas/ y los scripts a /usr/local/bin/. En ese caso los scripts ya no están junto a sus bibliotecas, y deben buscarlas en varias rutas posibles: primero junto al script, para el desarrollo, y después en /usr/local/lib/herramientas.
10. Bibliotecas y ShellCheck
Cuando ShellCheck encuentra un source con una ruta que se calcula al ejecutar, no puede saber qué archivo es, y avisa con SC1091. Además, sin leer la biblioteca, no sabe qué funciones y variables define. Como vimos en el tema 4, se le indica la ruta con una directiva, relativa al script:
# shellcheck source=../lib/comun.sh
source "$DIR_BASE/lib/comun.sh"
Y se ejecuta con shellcheck -x bin/*, que le permite seguir los source. Las bibliotecas, que no tienen línea shebang, se revisan con shellcheck -s bash lib/*.sh, para indicarle el dialecto.
11. Un proyecto completo
El directorio de descargas acumula archivos que nadie vuelve a mirar. Este proyecto contiene dos bibliotecas y un script que las usa para mover las descargas de más de un mes a subdirectorios por mes, como antiguos/2026-09/, informando cuánto espacio ordenó.
# comun.sh — funciones generales para los scripts del proyecto.
# Se carga con «source»; no ejecuta nada por sí misma.
[[ -n "${_COMUN_SH:-}" ]] && return 0
_COMUN_SH=1
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
aviso() {
printf '%s: aviso: %s\n' "${0##*/}" "$*" >&2
}
morir() {
error "$@"
exit 1
}
requerir() {
local programa
for programa in "$@"; do
command -v "$programa" > /dev/null || morir "falta el programa $programa"
done
}
confirmar() {
local respuesta
read -rp "$1 [s/N] " respuesta
[[ "${respuesta,,}" == s* ]]
}
# archivos.sh — funciones para trabajar con archivos.
# Se carga con «source»; no ejecuta nada por sí misma.
[[ -n "${_ARCHIVOS_SH:-}" ]] && return 0
_ARCHIVOS_SH=1
# a_legible BYTES — escribe el tamaño en unidades legibles: «1.5 KiB».
a_legible() {
local bytes="$1"
local -a unidades=(B KiB MiB GiB TiB)
local i=0 decimas=$(( bytes * 10 ))
while (( decimas >= 10240 && i < ${#unidades[@]} - 1 )); do
decimas=$(( decimas / 1024 ))
i=$(( i + 1 ))
done
if (( i == 0 )); then
printf '%d B\n' "$bytes"
else
printf '%d.%d %s\n' $(( decimas / 10 )) $(( decimas % 10 )) "${unidades[i]}"
fi
}
# mes_de ARCHIVO — escribe el mes de modificación del archivo: «2026-09».
mes_de() {
date -r "$1" +%Y-%m
}
#!/usr/bin/env bash
#
# archivar-descargas — mueve las descargas antiguas a subdirectorios por mes.
#
# Uso: archivar-descargas [-n] [-d DÍAS] [DIRECTORIO] (30 días; ~/Descargas)
#
set -euo pipefail
readonly DIR_BASE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
# shellcheck source=../lib/comun.sh
source "$DIR_BASE/lib/comun.sh"
# shellcheck source=../lib/archivos.sh
source "$DIR_BASE/lib/archivos.sh"
uso() {
echo "Uso: ${0##*/} [-n] [-d DÍAS] [DIRECTORIO]" >&2
exit 2
}
main() {
local simular=0 dias=30 opcion
while getopts ":nd:" opcion; do
case "$opcion" in
n) simular=1 ;;
d) dias="$OPTARG" ;;
*) uso ;;
esac
done
shift $(( OPTIND - 1 ))
[[ "$dias" =~ ^[0-9]+$ ]] || { error "-d debe ser un número: $dias"; uso; }
local dir="${1:-$HOME/Descargas}"
[[ -d "$dir" ]] || morir "no existe el directorio $dir"
requerir find date stat
local archivo destino tam movidos=0 bytes=0
while IFS= read -r -d '' archivo; do
destino="$dir/antiguos/$(mes_de "$archivo")"
tam=$(stat -c %s -- "$archivo")
if (( simular )); then
echo "movería ${archivo##*/} → antiguos/${destino##*/}/"
else
mkdir -p -- "$destino"
mv -n -- "$archivo" "$destino/"
fi
movidos=$(( movidos + 1 ))
bytes=$(( bytes + tam ))
done < <(find "$dir" -maxdepth 1 -type f -mtime +"$dias" -print0)
echo "Archivos archivados: $movidos ($(a_legible "$bytes"))"
}
main "$@"
$ ln -s ~/proyectos/herramientas/bin/archivar-descargas ~/bin/
$ archivar-descargas -n
movería factura-agosto.pdf → antiguos/2026-08/
movería ubuntu-24.04.iso → antiguos/2026-09/
Archivos archivados: 2 (1.4 GiB)
$ archivar-descargas
Archivos archivados: 2 (1.4 GiB)
$ archivar-descargas # una segunda vez: ya no queda nada viejo
Archivos archivados: 0 (0 B)
- El script se ejecuta mediante un enlace en
~/biny aun así encuentra sus bibliotecas, gracias areadlink -f. - Las bibliotecas solo definen: no activan opciones ni hacen nada al cargarse. El
set -euo pipefailestá en el script, que es quien decide. - Las dos bibliotecas tienen su guarda, de modo que un tercer archivo que cargara
comun.shno la cargaría dos veces. mainusa funciones de las dos bibliotecas (error,morir,requerir,a_legible,mes_de) sin definir ninguna. Un segundo script del proyecto, comorespaldar, las reutilizaría tal cual.date -r ARCHIVOda la fecha de modificación de un archivo con el formato indicado, en lugar de la fecha actual.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
source ./lib/comun.sh | Funciona solo desde el directorio del proyecto. | Una ruta construida desde BASH_SOURCE. |
dirname "$0" para ubicar la biblioteca | Falla si el script se carga con source o se ejecuta mediante un enlace. | readlink -f "${BASH_SOURCE[0]}" |
set -e o exit en el cuerpo de una biblioteca | Cambian el comportamiento del script que la carga, o lo terminan. | Solo definiciones en la biblioteca. |
Una biblioteca sin guarda, con readonly | La segunda carga falla con readonly variable. | La guarda con return 0. |
exit 0 como guarda | Termina el script entero. | return 0. |
| Funciones con nombres genéricos en una biblioteca compartida | Pisan funciones del script, sin aviso. | Un prefijo: log_info. |
shellcheck bin/script con bibliotecas | SC1091, y avisos de funciones «no definidas». | Directiva source= y shellcheck -x. |
13. Resumen
- Una biblioteca es un archivo con funciones que los scripts cargan con
source; sus definiciones quedan en el script, y$0sigue siendo el del script. - La biblioteca se ubica a partir de
${BASH_SOURCE[0]}, la ruta del archivo actual, convertida en absoluta concdypwd. readlink -fresuelve los enlaces, para que un script instalado mediante un enlace encuentre sus bibliotecas.- Una biblioteca solo define funciones y constantes: no activa opciones, no ejecuta acciones y no llama a
exit. - Una guarda al comienzo,
[[ -n "${_LIB:-}" ]] && return 0, evita cargarla dos veces. - Un prefijo en los nombres evita choques; el guion bajo inicial marca lo interno.
[[ "${BASH_SOURCE[0]}" == "$0" ]]distingue si un archivo se ejecuta o se carga, y permite probar sus funciones por separado.- Un proyecto se organiza en
bin/para los ejecutables, sin extensión, ylib/para las bibliotecas, con.sh, idealmente en un repositorio Git. - Para ShellCheck, una directiva
source=por cada biblioteca, yshellcheck -x.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Dentro de una biblioteca cargada con source, ¿qué contiene $0?
El nombre del script que la cargó, porque source no crea un proceso nuevo. Por eso las funciones de error de una biblioteca muestran el nombre del script.
2. ¿Por qué source ./lib/comun.sh no es una buena forma de cargar una biblioteca?
Porque la ruta es relativa al directorio actual, no al del script. Si el script se ejecuta desde otro directorio, no encuentra la biblioteca.
3. ¿Qué diferencia hay entre $0 y ${BASH_SOURCE[0]}?
$0 es siempre la ruta con que se invocó el script. ${BASH_SOURCE[0]} es la ruta del archivo que contiene la línea en ejecución: en una biblioteca, la de la biblioteca.
4. ¿Para qué sirve readlink -f al ubicar las bibliotecas?
Para resolver los enlaces simbólicos. Si el script se instaló como un enlace en ~/bin, BASH_SOURCE contiene la ruta del enlace; readlink -f da la del archivo real, junto al cual están las bibliotecas.
5. ¿Por qué una biblioteca no debe contener set -euo pipefail?
Porque se ejecuta dentro del script que la carga, y cambiaría su comportamiento. Las opciones de la shell son una decisión del script.
6. ¿Cómo funciona la guarda [[ -n "${_COMUN_SH:-}" ]] && return 0?
La primera vez la variable no existe, la biblioteca se carga y la define. La segunda, la variable existe y return termina la carga del archivo sin volver a definir nada.
7. ¿Por qué la guarda usa return y no exit?
Porque en un archivo cargado con source, return termina solo la carga del archivo, mientras que exit terminaría el script que lo cargó.
8. ¿Para qué sirve if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi?
Para que el archivo ejecute main solo cuando se lo ejecuta directamente. Si se lo carga con source, por ejemplo desde una prueba, define sus funciones sin ejecutar nada.
9. ¿Por qué los scripts de bin/ no llevan extensión y las bibliotecas sí?
Porque los scripts son comandos, y a quien los usa no le importa en qué lenguaje están escritos. Las bibliotecas no son comandos: se cargan con source y la extensión indica que son código de shell.
10. ¿Cómo se le indica a ShellCheck dónde está una biblioteca cargada con una ruta calculada?
Con una directiva # shellcheck source=ruta/relativa.sh antes del source, y ejecutándolo con shellcheck -x.
Para practicar
- Crea la estructura de directorios de la sección 8 en
~/proyectos/herramientasy copia allí las dos bibliotecas yarchivar-descargas. - Ejecuta
archivar-descargas -ndesde el directorio del proyecto, desde tu directorio personal y mediante un enlace en~/bin. - Reemplaza en el script la línea de
DIR_BASEpor una que usedirname "$0", y comprueba en qué caso deja de funcionar. - Carga
comun.shdos veces en una misma sesión y comprueba condeclare -Fque las funciones están definidas una sola vez. Quita la guarda, agrega una constante conreadonlyy repite. - Convierte alguno de los scripts de temas anteriores, como
respaldardel tema 18, para que usecomun.shen lugar de definir sus propias funciones. - Agrega a
archivos.shuna función y escribe un pequeño script de prueba que la cargue y compruebe su resultado con varios valores. - Pasa
shellcheck -xpor el script yshellcheck -s bashpor las bibliotecas.