Creación de Bibliotecas de Funciones y Reutilización en Bash

DUGLAS MORENODUGLAS MORENO 👁 46

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

  1. Introducción
  2. Anatomía de una función en Bash
  3. Tu primera biblioteca: source vs ejecución
  4. Estructura de una biblioteca profesional
  5. Convenciones y buenas prácticas
  6. Ejemplo completo: biblioteca logger.sh
  7. Ejemplo completo: biblioteca utils.sh
  8. Cargando múltiples bibliotecas: un loader
  9. Instalación global de tu biblioteca
  10. Testing de funciones con bats
  11. 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, Mundo

Puntos clave:

  • Los argumentos se acceden con $1, $2, ..., $@, $#.
  • El "valor de retorno" (return N) es un código de salida entre 0 y 255, no un valor.
  • Para "devolver" datos usamos echo/printf y capturamos con $(...).
  • Las variables son globales por defecto; usa local dentro de la función.
sumar() {
    local a="$1"
    local b="$2"
    echo $(( a + b ))
}

resultado=$(sumar 3 4)
echo "$resultado"   # → 7

3. 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:

  1. Cabecera con nombre, autor, versión y modo de uso.
  2. Guarda contra doble carga (__LOGGER_SH_LOADED): evita que si dos scripts cargan la misma librería se redefinan cosas.
  3. Constantes readonly con prefijo consistente.
  4. Funciones públicas con un prefijo del módulo (log_*).

5. Convenciones y buenas prácticas

Nomenclatura

  • Nombres de funciones en snake_case con prefijo del módulo: logger_info, db_connect, http_get.
  • Constantes MAYÚSCULAS con 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 de 0 (error) con return.
  • Los datos se devuelven vía echo/printf para 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 datos

7. 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.bats

lib/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 jq

El 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 install

10. 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"            # → 7

No 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=1

Rutas relativas frágiles

source ./lib/logger.sh   # ❌ falla si el script no se ejecuta desde su carpeta
readonly HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$HERE/lib/logger.sh"   # ✅

Recursos


¡Feliz scripting! 🐚

Con estas piezas ya puedes ir separando tu código Bash en módulos limpios, testeables y reutilizables entre proyectos.

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario