Creación de Bibliotecas de Funciones y Reutilización en Bash
Creación de Bibliotecas de Funciones y Reutilización en Bash
Tutorial práctico para organizar, reutilizar y compartir código Bash de forma profesional.
Tabla de contenido
- Introducción
- Anatomía de una función en Bash
- Tu primera biblioteca:
sourcevs ejecución - Estructura de una biblioteca profesional
- Convenciones y buenas prácticas
- Ejemplo completo: biblioteca
logger.sh - Ejemplo completo: biblioteca
utils.sh - Cargando múltiples bibliotecas: un
loader - Instalación global de tu biblioteca
- Testing de funciones con
bats - Errores comunes y cómo evitarlos
1. Introducción
Cuando escribimos scripts en Bash, es muy común copiar y pegar las mismas funciones (logs, validaciones, manejo de errores, wrappers de curl) entre proyectos. Eso lleva a duplicación, inconsistencias y bugs.
Una biblioteca de funciones es simplemente un archivo .sh que agrupa funciones reutilizables. En lugar de ejecutarlo, lo cargas en otros scripts con source (o su alias .) para que sus funciones queden disponibles en el shell actual.
Ventajas:
- Reutilización de código entre proyectos.
- Un único lugar donde arreglar bugs.
- Scripts principales cortos y legibles.
- Facilita testing unitario.
2. Anatomía de una función en Bash
Antes de crear bibliotecas, refresquemos la sintaxis:
# Forma clásica (portable, POSIX)
mi_funcion() {
echo "Hola, $1"
}
# Forma con la palabra clave 'function' (solo Bash/Ksh)
function mi_funcion {
echo "Hola, $1"
}
# Uso
mi_funcion "Mundo" # → Hola, MundoPuntos clave:
- Los argumentos se acceden con
$1,$2, ...,$@,$#. - El "valor de retorno" (
return N) es un código de salida entre0y255, no un valor. - Para "devolver" datos usamos
echo/printfy capturamos con$(...). - Las variables son globales por defecto; usa
localdentro de la función.
sumar() {
local a="$1"
local b="$2"
echo $(( a + b ))
}
resultado=$(sumar 3 4)
echo "$resultado" # → 73. Tu primera biblioteca: source vs ejecución
Crea saludos.sh:
#!/usr/bin/env bash
# saludos.sh - Biblioteca de saludos
saludar() {
echo "¡Hola, $1!"
}
despedir() {
echo "Adiós, $1."
}Ahora, en otro script main.sh:
#!/usr/bin/env bash
# main.sh
# Cargamos la biblioteca (source == .)
source ./saludos.sh
# o equivalente: . ./saludos.sh
saludar "Ada"
despedir "Ada"Ejecutamos:
chmod +x main.sh
./main.sh
# ¡Hola, Ada!
# Adiós, Ada.Diferencia clave
| Acción | Efecto |
|---|---|
./saludos.sh |
Se ejecuta en un subshell; sus funciones no quedan disponibles al terminar. |
source saludos.sh |
Se ejecuta en el shell actual; sus funciones y variables quedan disponibles. |
Por eso las bibliotecas se cargan, no se ejecutan.
4. Estructura de una biblioteca profesional
Una biblioteca bien diseñada suele verse así:
#!/usr/bin/env bash
# ==============================================================================
# logger.sh - Utilidades de logging
# Autor: gpg
# Versión: 1.0.0
# Uso: source logger.sh
# ==============================================================================
# --- Guarda contra doble carga ---------------------------------------------
[[ -n "${__LOGGER_SH_LOADED:-}" ]] && return 0
readonly __LOGGER_SH_LOADED=1
# --- Constantes ------------------------------------------------------------
readonly LOG_COLOR_RED='\033[0;31m'
readonly LOG_COLOR_GREEN='\033[0;32m'
readonly LOG_COLOR_YELLOW='\033[0;33m'
readonly LOG_COLOR_RESET='\033[0m'
# --- Funciones públicas ----------------------------------------------------
log_info() { printf "${LOG_COLOR_GREEN}[INFO]${LOG_COLOR_RESET} %s\n" "$*"; }
log_warn() { printf "${LOG_COLOR_YELLOW}[WARN]${LOG_COLOR_RESET} %s\n" "$*" >&2; }
log_error() { printf "${LOG_COLOR_RED}[ERROR]${LOG_COLOR_RESET} %s\n" "$*" >&2; }Elementos que aparecen:
- Cabecera con nombre, autor, versión y modo de uso.
- Guarda contra doble carga (
__LOGGER_SH_LOADED): evita que si dos scripts cargan la misma librería se redefinan cosas. - Constantes
readonlycon prefijo consistente. - Funciones públicas con un prefijo del módulo (
log_*).
5. Convenciones y buenas prácticas
Nomenclatura
- Nombres de funciones en
snake_casecon prefijo del módulo:logger_info,db_connect,http_get. - Constantes
MAYÚSCULAScon prefijo (LOG_*,DB_*). - Variables internas de una función siempre con
local.
"Privado" vs "público"
- Bash no tiene visibilidad, pero por convención se prefijan las funciones internas con
_o__:_http_build_url() { ... } # interna http_get() { ... } # pública
Modo estricto
Al inicio del script que usa la biblioteca, activa modo estricto:
set -euo pipefail
IFS=$'\n\t'-e: sale si un comando falla.-u: sale si usas una variable no definida.-o pipefail: el pipeline falla si cualquier etapa falla.
No hagas set -e dentro de la biblioteca: la decisión pertenece al script que la usa.
Retornos
- Devuelve
0(éxito) o distinto de0(error) conreturn. - Los datos se devuelven vía
echo/printfpara capturarlos con$(...).
6. Ejemplo completo: biblioteca logger.sh
lib/logger.sh:
#!/usr/bin/env bash
# ==============================================================================
# logger.sh - Logging con niveles y colores
# ==============================================================================
[[ -n "${__LOGGER_SH_LOADED:-}" ]] && return 0
readonly __LOGGER_SH_LOADED=1
# Nivel actual: DEBUG=0, INFO=1, WARN=2, ERROR=3
: "${LOG_LEVEL:=1}"
readonly _LOG_C_RED='\033[0;31m'
readonly _LOG_C_YELLOW='\033[0;33m'
readonly _LOG_C_GREEN='\033[0;32m'
readonly _LOG_C_BLUE='\033[0;34m'
readonly _LOG_C_RESET='\033[0m'
_log() {
local level="$1"; shift
local color="$1"; shift
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
printf "${color}[%s] [%s]${_LOG_C_RESET} %s\n" "$ts" "$level" "$*"
}
log_debug() { (( LOG_LEVEL <= 0 )) && _log "DEBUG" "$_LOG_C_BLUE" "$@"; return 0; }
log_info() { (( LOG_LEVEL <= 1 )) && _log "INFO" "$_LOG_C_GREEN" "$@"; return 0; }
log_warn() { (( LOG_LEVEL <= 2 )) && _log "WARN" "$_LOG_C_YELLOW" "$@" >&2; return 0; }
log_error() { (( LOG_LEVEL <= 3 )) && _log "ERROR" "$_LOG_C_RED" "$@" >&2; return 0; }Uso en demo_logger.sh:
#!/usr/bin/env bash
set -euo pipefail
source ./lib/logger.sh
LOG_LEVEL=0 # DEBUG
log_debug "Cargando configuración..."
log_info "Aplicación iniciada correctamente"
log_warn "El disco está al 80% de uso"
log_error "No se pudo conectar a la base de datos"Salida:
[2026-07-02 10:15:03] [DEBUG] Cargando configuración...
[2026-07-02 10:15:03] [INFO] Aplicación iniciada correctamente
[2026-07-02 10:15:03] [WARN] El disco está al 80% de uso
[2026-07-02 10:15:03] [ERROR] No se pudo conectar a la base de datos7. Ejemplo completo: biblioteca utils.sh
lib/utils.sh — utilidades genéricas:
#!/usr/bin/env bash
# ==============================================================================
# utils.sh - Utilidades varias
# ==============================================================================
[[ -n "${__UTILS_SH_LOADED:-}" ]] && return 0
readonly __UTILS_SH_LOADED=1
# ¿Existe un comando en el PATH?
utils_command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Requiere un comando o aborta
utils_require() {
local cmd="$1"
if ! utils_command_exists "$cmd"; then
echo "ERROR: se requiere el comando '$cmd' y no está instalado." >&2
return 127
fi
}
# Confirmación interactiva (s/n)
utils_confirm() {
local prompt="${1:-¿Continuar?}"
local reply
read -r -p "$prompt [s/N] " reply
[[ "$reply" =~ ^[sSyY]$ ]]
}
# Reintenta un comando N veces con backoff
utils_retry() {
local max="${1:-3}"; shift
local delay=1
local attempt=1
until "$@"; do
if (( attempt >= max )); then
return 1
fi
sleep "$delay"
delay=$(( delay * 2 ))
attempt=$(( attempt + 1 ))
done
}
# Trim de espacios
utils_trim() {
local s="$*"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}Uso:
#!/usr/bin/env bash
set -euo pipefail
source ./lib/utils.sh
source ./lib/logger.sh
utils_require curl || exit $?
if utils_confirm "¿Descargar el archivo?"; then
utils_retry 3 curl -fsSLO https://example.com/archivo.tar.gz \
&& log_info "Descarga completa" \
|| log_error "Falló tras 3 intentos"
fi
nombre=$(utils_trim " Ada Lovelace ")
log_info "Nombre limpio: '$nombre'"8. Cargando múltiples bibliotecas: un loader
Cuando tienes varias bibliotecas, crea un único punto de entrada.
Estructura sugerida del proyecto:
mi-proyecto/
├── bin/
│ └── mi-cli
├── lib/
│ ├── loader.sh
│ ├── logger.sh
│ ├── utils.sh
│ └── http.sh
└── tests/
└── test_utils.batslib/loader.sh:
#!/usr/bin/env bash
# loader.sh - Carga todas las bibliotecas del proyecto
[[ -n "${__LOADER_SH_LOADED:-}" ]] && return 0
readonly __LOADER_SH_LOADED=1
# Directorio donde vive este archivo, aunque nos hagan 'source' desde otro lado
readonly LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$LIB_DIR/logger.sh"
source "$LIB_DIR/utils.sh"
source "$LIB_DIR/http.sh"bin/mi-cli:
#!/usr/bin/env bash
set -euo pipefail
# Resolvemos la raíz del proyecto de forma robusta
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$PROJECT_ROOT/lib/loader.sh"
log_info "CLI iniciada desde $PROJECT_ROOT"
utils_require jqEl truco de "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" te da la ruta absoluta del script actual, funcione desde donde funcione la llamada.
9. Instalación global de tu biblioteca
Si quieres poder hacer source logger.sh desde cualquier lado:
Opción A: variable de entorno
# ~/.bashrc
export MY_BASH_LIB="$HOME/.local/share/bash-lib"En tus scripts:
source "$MY_BASH_LIB/logger.sh"Opción B: añadir al PATH de bibliotecas con una función bimport
Añade a tu ~/.bashrc:
export BASH_LIB_PATH="$HOME/.local/share/bash-lib:/usr/local/share/bash-lib"
bimport() {
local name="$1"
local IFS=':'
for dir in $BASH_LIB_PATH; do
if [[ -r "$dir/$name.sh" ]]; then
source "$dir/$name.sh"
return 0
fi
done
echo "bimport: no se encontró '$name'" >&2
return 1
}Uso:
bimport logger
bimport utils
log_info "Todo cargado"Opción C: paquete instalable
Un Makefile mínimo:
PREFIX ?= /usr/local
LIBDIR = $(PREFIX)/share/bash-lib
install:
install -d $(LIBDIR)
install -m 644 lib/*.sh $(LIBDIR)/
uninstall:
rm -rf $(LIBDIR)sudo make install10. Testing de funciones con bats
bats-core permite escribir tests unitarios de Bash.
tests/test_utils.bats:
#!/usr/bin/env bats
setup() {
source "${BATS_TEST_DIRNAME}/../lib/utils.sh"
}
@test "utils_trim quita espacios" {
result="$(utils_trim ' hola ')"
[ "$result" = "hola" ]
}
@test "utils_command_exists detecta comandos reales" {
run utils_command_exists bash
[ "$status" -eq 0 ]
}
@test "utils_command_exists falla con comandos inexistentes" {
run utils_command_exists comando_que_no_existe_1234
[ "$status" -ne 0 ]
}
@test "utils_retry reintenta y termina en éxito" {
counter_file="$(mktemp)"
echo 0 > "$counter_file"
fake_cmd() {
local n
n=$(cat "$counter_file")
n=$((n + 1))
echo "$n" > "$counter_file"
[ "$n" -ge 2 ] # falla la 1ra, éxito la 2da
}
run utils_retry 3 fake_cmd
[ "$status" -eq 0 ]
[ "$(cat "$counter_file")" = "2" ]
}Ejecutar:
bats tests/11. Errores comunes y cómo evitarlos
Ejecutar en lugar de sourcear
./logger.sh # ❌ funciones no disponibles después
source ./logger.sh # ✅Variables globales sin querer
mi_funcion() {
tmp="algo" # ❌ contamina el shell
local tmp="algo" # ✅
}Confundir return con "devolver un valor"
sumar() { return $(( $1 + $2 )); } # ❌ solo 0-255, y es "exit code"
r=$(sumar 3 4); echo "$r" # imprime nada
sumar() { echo $(( $1 + $2 )); } # ✅
r=$(sumar 3 4); echo "$r" # → 7No entrecomillar variables
rm $archivo # ❌ se rompe con espacios o globs
rm -- "$archivo" # ✅set -e dentro de la biblioteca
Deja que el script que consume la biblioteca decida el modo estricto. Si activas set -e dentro, cambias el comportamiento de scripts que te importan sin avisar.
Doble carga y readonly
Si defines constantes readonly y la biblioteca se carga dos veces, Bash lanzará errores del tipo readonly variable. Usa siempre la guarda:
[[ -n "${__MI_LIB_LOADED:-}" ]] && return 0
readonly __MI_LIB_LOADED=1Rutas relativas frágiles
source ./lib/logger.sh # ❌ falla si el script no se ejecuta desde su carpetareadonly HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$HERE/lib/logger.sh" # ✅Recursos
- Guía de estilo Bash de Google: https://google.github.io/styleguide/shellguide.html
- ShellCheck (linter imprescindible): https://www.shellcheck.net/
bats-corepara testing: https://github.com/bats-core/bats-core- Bash Reference Manual: https://www.gnu.org/software/bash/manual/
¡Feliz scripting! 🐚
Con estas piezas ya puedes ir separando tu código Bash en módulos limpios, testeables y reutilizables entre proyectos.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.