Tienes cuatro scripts en ~/scripts y log() está escrita cuatro veces. error() también. ejecutar() va por su tercera copia. El día que decidas que el registro debe llevar el nivel además de la fecha tendrás que editar cuatro ficheros y acordarte de los cuatro; y el día que se te olvide uno, tendrás un script que registra distinto sin que nadie se entere hasta que haga falta. Las funciones resuelven eso, pero no solo eso: una función bien nombrada convierte diez líneas crípticas en una línea que se lee como una frase. Esta lección va de cuándo extraerlas, de las dos trampas que Bash reserva a quien viene de otros lenguajes —el ámbito dinámico y el return que solo devuelve números— y de cómo montar una librería de verdad, con carga robusta, guardas y tests. Al terminar existirá lib/comunes.sh y los scripts anteriores la usarán.
Contenido
- Cuándo extraer una función
- Sintaxis, y por qué no usamos
function - Los argumentos de una función son suyos
- Ámbito:
localy el ámbito dinámico de Bash - Devolver valores:
returnsolo devuelve un número - Funciones predicado
- Recursión, y sobrescribir comandos
- Librerías:
lib/comunes.sh - Qué debe y qué no debe hacer una librería
- Tests mínimos sin frameworks
- Aplicación: refactorizar los scripts
- Cuándo extraer una función
Tres criterios, y basta con que se cumpla uno:
- La regla de tres. Has escrito lo mismo por tercera vez: la primera puede ser casualidad, la segunda impaciencia; la tercera es una función.
- El bloque necesita un comentario para entenderse. Si ibas a escribir
# comprueba si el release existe y no está vacío, ese comentario es en realidad el nombre:existe_release(). Y si hay más de dos niveles de anidamiento, como avisábamos en 04-04, un tercer nivel casi siempre es una función que falta.
El nombre importa tanto como el código: un buen nombre es un verbo o un predicado, describe qué hace y no cómo, y hace que la llamada se lea sin comentarios. Compara if [[ -d /opt/tramontana/releases/$v && -f /opt/tramontana/releases/$v/app.jar ]] con if existe_release "$v". Y un aviso de equilibrio: una función de una línea usada una sola vez suele ser ruido. La medida no es «cuantas más, mejor», sino «que cada nombre ahorre una explicación».
- Sintaxis, y por qué no usamos
function
functionBash admite nombre() { cuerpo; } (POSIX), function nombre { cuerpo; } (extensión de Bash/ksh) y la mezcla function nombre() { ... }. Usamos siempre la primera: es la única que funciona en cualquier shell POSIX, es la que espera shellcheck por defecto y no aporta nada ahorrarse los paréntesis. Detalles de sintaxis que dan errores misteriosos:
- El cuerpo entre
{ }necesita espacios después de{y antes de}, porque{es una palabra reservada, no un símbolo, y la última instrucción antes de}necesita;o un salto de línea. - Las funciones deben definirse antes de usarse: Bash lee el fichero de arriba abajo. De ahí que en 04-07 pongamos
main "$@"como última línea del script. - Una función y un alias con el mismo nombre chocan, y gana el alias: otra razón para no abusar de ellos.
declare -f nombre muestra el código de una función ya definida y declare -F lista los nombres: son el equivalente de declare -p para variables, muy útiles cuando dudas de qué versión se ha cargado.
- Los argumentos de una función son suyos
Dentro de una función, $1, $@ y $# no son los del script: son los de la llamada. $0, en cambio, sigue siendo el nombre del script.
operador@srv-tramontana:~$ cat /tmp/args_fn.sh
mostrar() { printf 'fn: $#=%d $1=%s $0=%s\n' "$#" "${1:-vacío}" "${0##*/}"; }
printf 'script: $#=%d $1=%s\n' "$#" "${1:-vacío}"
mostrar alfa beta
mostrar # sin argumentos
operador@srv-tramontana:~$ bash /tmp/args_fn.sh 3.2.1
script: $#=1 $1=3.2.1
fn: $#=2 $1=alfa $0=args_fn.sh
fn: $#=0 $1=vacío $0=args_fn.shEs la confusión número uno de quien empieza: llamar a la función esperando que «vea» los argumentos del script. Si quieres pasárselos, hazlo explícitamente con mostrar "$@", que ya sabes de 04-03 que es la única forma que respeta los espacios. Y fíjate en ${1:-vacío}: con set -u activo, leer $1 en una función sin argumentos aborta el script, así que un valor por defecto es obligatorio si el argumento puede faltar.
- Ámbito:
local y el ámbito dinámico de Bash
local y el ámbito dinámico de BashToda variable creada dentro de una función debe declararse local. Sin local, la variable es global y sobrevive a la función, con consecuencias divertidas:
operador@srv-tramontana:~$ contar() { for i in 1 2 3; do :; done; }
operador@srv-tramontana:~$ for i in a b c; do contar; printf '%s ' "$i"; done; echo
3 3 3
operador@srv-tramontana:~$ contar() { local i; for i in 1 2 3; do :; done; }
operador@srv-tramontana:~$ for i in a b c; do contar; printf '%s ' "$i"; done; echo
a b cEn el primer caso contar pisó la i del llamante y el bucle exterior imprimió tres veces el 3 en vez de a b c. Un local i lo arregla. Con nombres genéricos como i, n, tmp o linea esto pasa constantemente.
Ahora la parte que sorprende a quien viene de otros lenguajes. Bash tiene ámbito dinámico, no léxico: una variable local es visible también dentro de las funciones que esa función llame, aunque estén definidas en otro fichero.
operador@srv-tramontana:~$ interior() { echo "interior ve: ${secreto:-nada}"; }
operador@srv-tramontana:~$ exterior() { local secreto="visible"; interior; }
operador@srv-tramontana:~$ exterior; interior
interior ve: visible
interior ve: nadaEn un lenguaje de ámbito léxico —Python, Java, C— interior no vería nunca esa variable, porque solo ve lo que hay en el texto que la rodea; en Bash ve lo que haya en la pila de llamadas. Tiene un uso legítimo —pasar contexto sin argumentos— pero es sobre todo acoplamiento invisible: no te apoyes en ello, declara local en todas partes y pasa los datos por argumentos.
- Devolver valores:
return solo devuelve un número
return solo devuelve un númerooperador@srv-tramontana:~$ sumar() { return $(( $1 + $2 )); }
operador@srv-tramontana:~$ sumar 200 100; echo "$?"
44300 se convirtió en 44, porque return acepta solo un entero de 0 a 255 y el valor se trunca módulo 256. return no es «devolver un resultado»: es «devolver un código de salida», exactamente el mismo concepto de 04-01 y 04-04. Para devolver datos hay tres caminos:
| Forma | Cómo | Ventaja | Inconveniente |
|---|---|---|---|
stdout + $( ) |
printf dentro, x=$(fn) fuera |
Componible, sin efectos secundarios | Crea un subshell: lento en bucles grandes |
| Global convenida | La función asigna RESULTADO=... |
Rápida, sin subshell | Contamina el espacio de nombres |
Nameref (local -n) |
El llamante pasa el nombre de su variable | Explícita y sin subshell | Bash ≥ 4.3; colisiona si los nombres coinciden |
# stdout: la forma preferente, y la única realmente componible
ruta_release() { printf '%s\n' "/opt/tramontana/releases/$1"; }
r=$(ruta_release 3.2.1)
# nameref: cuando devuelves varios valores o estás en un bucle apretado
partir_version() {
local -n _mayor="$2" _menor="$3" # _mayor pasa a SER la variable del llamante
local resto="${1#*.}"
_mayor="${1%%.*}"; _menor="${resto%%.*}"
}
partir_version "3.10.2" may men
printf 'mayor=%s menor=%s\n' "$may" "$men" # -> mayor=3 menor=10El guion bajo de _mayor no es capricho: si el llamante tuviera una variable llamada exactamente igual que el nameref, Bash daría referencia de variable circular. Prefijar los namerefs con _ reduce esa colisión a la práctica imposibilidad.
Regla del curso: stdout por defecto, y nameref solo cuando devuelvas varios valores o el subshell sea un problema medido, no imaginado.
- Funciones predicado
Una función que solo responde sí o no debe devolver 0 para «sí» y usarse directamente en un if, sin == true ni variables intermedias.
# es_numero CADENA -> 0 si es un entero no negativo, 1 si no.
es_numero() { [[ ${1:-} =~ ^[0-9]+$ ]]; }
# existe_release VERSION -> 0 si el release existe y no está vacío.
existe_release() {
local dir="/opt/tramontana/releases/${1:-}"
[[ -d $dir ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]
}
operador@srv-tramontana:~$ es_numero 80 && echo sí; es_numero "8O" || echo no
sí
no
operador@srv-tramontana:~$ if existe_release 3.2.1; then echo "listo"; fi
listoNinguna de las dos lleva return: una función devuelve el código de su última instrucción, y la última instrucción es justamente el test. Escribir if ...; then return 0; else return 1; fi es cuatro veces más largo y hace lo mismo.
Un aviso que enlaza con 04-06: si la última instrucción puede fallar legítimamente —un (( contador )) que valga 0, un grep sin coincidencias— la función devolverá 1 y, con set -e, tumbará el script. Por eso log() acaba con return 0 explícito.
- Recursión, y sobrescribir comandos
Bash admite recursión, pero rara vez es la respuesta: no hay optimización de llamadas de cola, FUNCNEST limita la profundidad, y casi todo lo recursivo que hace un administrador —recorrer un árbol de directorios— lo resuelve find mejor y más rápido. Lo que sí es útil es sobrescribir un comando con una función, típicamente para añadir seguridad o registro; la clave es poder llamar al original:
rm() {
error "usa 'ejecutar rm' en vez de rm directo, para respetar --dry-run"
command rm "$@" # command salta las funciones y llama al programa real
}command ignora funciones y alias y ejecuta el binario o builtin; builtin fuerza el builtin del shell (útil si envuelves cd o echo). Sin uno de los dos, la función se llamaría a sí misma infinitamente. Y una advertencia: no sobrescribas comandos en una librería compartida; que rm haga cosas raras a diez metros de donde está escrito es una forma excelente de arruinarle la tarde a alguien.
- Librerías:
lib/comunes.sh
lib/comunes.shCreamos ~/scripts/lib/comunes.sh con las funciones que se repiten:
#!/usr/bin/env bash
#
# comunes.sh - Funciones compartidas por los scripts de Tramontana.
# Autor : Operador de sistemas <operador@srv-tramontana> Fecha: 2026-08-18
# NO se ejecuta: se carga con 'source'. Sin código de nivel superior más
# allá de definir funciones y de la guarda de inclusión.
# Guarda de inclusión múltiple: si dos scripts cargan la librería, 'return 0'
# aborta esta lectura sin abortar el script que hizo el source.
[[ -n ${TRAMONTANA_COMUNES_CARGADA:-} ]] && return 0
readonly TRAMONTANA_COMUNES_CARGADA=1
TRAMONTANA_VERBOSE="${TRAMONTANA_VERBOSE:-0}"
# log MENSAJE... -> stderr con marca de tiempo, solo si TRAMONTANA_VERBOSE=1.
# Devuelve: siempre 0 (para no romper 'set -e' cuando VERBOSE=0).
log() {
(( TRAMONTANA_VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2
return 0
}
# error MENSAJE... -> mensaje de error en stderr. Devuelve 0.
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
# morir CODIGO MENSAJE... -> registra el error y termina el SCRIPT con CODIGO.
morir() { local codigo="$1"; shift; error "$@"; exit "$codigo"; }
# requiere_comando NOMBRE...
# Devuelve: 0 si todos están disponibles; 1 si falta alguno (lo dice).
requiere_comando() {
local cmd faltan=0
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || { error "falta el comando: $cmd"; faltan=1; }
done
return "$faltan"
}
# confirmar PREGUNTA
# Devuelve: 0 si responde s/sí. Sin terminal (cron), 1 sin preguntar.
confirmar() {
local respuesta
[[ -t 0 ]] || { error "sin terminal interactiva: no confirmo"; return 1; }
read -r -p "${1:-¿Continuar?} [s/N] " respuesta
[[ ${respuesta,,} == s || ${respuesta,,} == si ]]
}
# formatear_bytes N -> imprime N bytes en la unidad legible más cercana.
formatear_bytes() {
LC_ALL=C awk -v b="${1:-0}" 'BEGIN {
split("B KiB MiB GiB TiB", u, " "); i = 1
while (b >= 1024 && i < 5) { b /= 1024; i++ }
printf (i == 1 ? "%d %s\n" : "%.1f %s\n"), b, u[i]
}'
}Cargarla de forma robusta
source lib/comunes.sh solo funciona si el directorio de trabajo es ~/scripts, y cron ejecuta desde $HOME. La solución canónica:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"Desmenuzada de dentro afuera:
${BASH_SOURCE[0]}es la ruta del fichero que se está leyendo. Se usa en vez de$0porque si el fichero se carga consource,$0sería el del script padre, no el suyo.dirnameextrae su directorio, que puede ser relativo.cd ... && pwdconvierte esa ruta relativa en absoluta y resuelve los.., todo dentro de$( ), es decir en un subshell: tu directorio de trabajo real no cambia. El&&garantiza que si elcdfalla no se ejecuta elpwd(SC2164 de 04-01).- El comentario
# shellcheck source=...le dice a ShellCheck dónde está el fichero para que pueda analizarlo; sin él avisa con SC1091.
- Qué debe y qué no debe hacer una librería
| Debe | No debe |
|---|---|
| Definir funciones y, como mucho, constantes | Ejecutar trabajo al cargarse |
| Llevar guarda de inclusión múltiple | Llamar a exit en el nivel superior |
| Documentar cada función: parámetros, salida, retorno | Escribir en stdout salvo que sea su resultado |
Usar local en todas sus variables |
Redefinir comandos del sistema |
| Prefijar lo que no sean funciones públicas | Depender del directorio de trabajo |
La prohibición de exit merece un matiz: una librería no debe llamarlo en su nivel superior —mataría el shell de quien la cargue con source, incluida tu terminal—, pero sí puede hacerlo dentro de una función cuya finalidad declarada sea terminar, como morir(). La diferencia está en quién decide: el autor de la librería no, el que llama sí.
Sobre los prefijos: en un proyecto propio, log y error son cómodos y aceptables; en una librería pensada para que otros la carguen, prefija todo (tram_log, tram_error), porque el día que alguien cargue tu librería y otra que también define log ganará la última leída, en silencio. Nosotros ya prefijamos las variables (TRAMONTANA_*), que es donde el choque sería más difícil de diagnosticar.
- Tests mínimos sin frameworks
Una librería sin pruebas es una librería que romperás sin enterarte. No hace falta framework: basta con ejecutar cada función con entradas conocidas y contar los fallos.
#!/usr/bin/env bash
#
# test_comunes.sh - Pruebas de lib/comunes.sh.
# Uso: test_comunes.sh -> 0 si todo pasa, 1 si hay algún fallo.
# Sin -e a propósito: queremos ejecutar TODOS los casos, no parar en el primero.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"
fallos=0
comprobar() { # comprobar DESCRIPCION ESPERADO OBTENIDO
if [[ $2 == "$3" ]]; then printf ' ok %s\n' "$1"
else printf ' FALLO %s: esperaba [%s], obtuve [%s]\n' "$1" "$2" "$3" >&2
(( ++fallos ))
fi
}
comprobar "bytes 0" "0 B" "$(formatear_bytes 0)"
comprobar "bytes 2048" "2.0 KiB" "$(formatear_bytes 2048)"
comprobar "bytes release" "97.0 MiB" "$(formatear_bytes 101711872)"
requiere_comando bash awk 2>/dev/null
comprobar "requiere_comando existente" "0" "$?"
requiere_comando comando_inexistente_xyz 2>/dev/null
comprobar "requiere_comando ausente" "1" "$?"
TRAMONTANA_VERBOSE=0; log "no debe verse" 2>/dev/null
comprobar "log callado devuelve 0" "0" "$?"
printf '\n%d fallo(s)\n' "$fallos"
(( fallos == 0 ))
operador@srv-tramontana:~$ ~/scripts/test_comunes.sh; echo "código: $?"
ok bytes 0
ok bytes 2048
ok bytes release
ok requiere_comando existente
ok requiere_comando ausente
ok log callado devuelve 0
0 fallo(s)
código: 0Treinta líneas y ya tienes una red de seguridad. El caso log callado devuelve 0 no es decorativo: comprueba el return 0 del apartado 6, el que evita que set -e mate el script cuando el modo detallado está desactivado. Los tests valen sobre todo para eso: fijar las decisiones sutiles antes de que alguien las «simplifique».
- Aplicación: refactorizar los scripts
Con la librería lista, la cabecera de revision_salud.sh e informe_reservas.sh queda idéntica y las funciones duplicadas desaparecen:
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"
requiere_comando curl df awk || morir 69 "faltan dependencias"
while getopts ":hvu:" opcion; do
case "$opcion" in
h) uso; exit 0 ;;
v) TRAMONTANA_VERBOSE=1 ;; # ahora la controla la librería
u) es_numero "$OPTARG" || morir 2 "el umbral debe ser un número: $OPTARG"
UMBRAL_DISCO="$OPTARG" ;;
\?) morir 2 "opción desconocida: -$OPTARG" ;;
:) morir 2 "la opción -$OPTARG necesita un valor" ;;
esac
done
shift $(( OPTIND - 1 ))
operador@srv-tramontana:~$ ~/scripts/revision_salud.sh -u ochenta; echo "código: $?"
[2026-08-18 12:03:44] ERROR: el umbral debe ser un número: ochenta
código: 2Tres ganancias concretas. morir 2 "..." sustituye a error ...; exit 2 en cinco sitios, así que ningún camino de error puede olvidarse del exit. La validación del umbral aparece por primera vez, y aparece porque es_numero ya existía y costaba una línea usarla. Y TRAMONTANA_VERBOSE es una sola variable en un solo fichero, en vez de cuatro VERBOSE que podían desincronizarse. Añade también purgar_releases.sh: su ejecutar() se muda a la librería tal cual, y su comprobación del release activo se convierte en existe_release "$activo" || morir 66 "el release activo no existe".
Errores Comunes y Consejos
- Olvidar
local. La variable se vuelve global y pisa la del llamante; con nombres comoi,tmpolineaes cuestión de tiempo. local x=$(comando)y creer que has comprobado el error. El código de salida que se ve es el delocal, no el del comando: siempre 0. Declara primero (local x) y asigna después (x=$(comando)). Es SC2155, y lo veremos a fondo en 04-06.- Esperar que
returndevuelva un dato. Solo devuelve 0-255, y 300 se convierte en 44: los datos van por stdout. Y ojo con usar los argumentos del script dentro de una función: dentro,$1es el de la función, así que pásalos con"$@". Define la función antes de usarla, porque Bash lee de arriba abajo y la llamada fallaría conorden no encontrada. - Poner código ejecutable en el nivel superior de una librería. Cargarla tendrá efectos que nadie pidió, y un
exitallí cerrará la terminal de quien la cargue a mano. Y no la cargues con ruta relativa: funciona en tu terminal y falla en cron, que arranca desde otro directorio; usaSCRIPT_DIR. - Consejo: documenta cada función con tres líneas de contrato —qué recibe, qué imprime, qué devuelve— justo encima: es lo que permite usarla sin leer el cuerpo. Y ejecuta
test_comunes.shantes de cada commit de la librería: diez segundos que evitan romper cuatro scripts a la vez. - Consejo: si una función necesita más de cinco argumentos o más de treinta líneas, probablemente son dos funciones.
Ejercicios
Ejercicio 1. Explica qué imprime este fragmento y por qué, y luego arréglalo para que imprima lo que su autor esperaba.
procesar() {
total=0
for n in "$@"; do total=$(( total + n )); done
return "$total"
}
total=1000
procesar 7 6 5 4 3
echo "reservas: $total"Ejercicio 2. Añade a lib/comunes.sh una función resumen_casa CSV CASA que imprima casa;reservas;noches;importe para una casa de reservas.csv, con código 0 si existe y 1 si no. Añade dos casos a test_comunes.sh: mas-figueres (debe dar mas-figueres;7;21;2450.00) y una casa inexistente.
Ejercicio 3. Luis ha escrito esta librería. Encuentra los cinco problemas y reescríbela.
#!/bin/sh
echo "cargando librería..."
LOGFILE=/tmp/tramontana.log
function log { echo "$1" >> $LOGFILE; }
function comprobar_disco {
uso=`df / | tail -1 | awk '{print $5}' | tr -d %`
if [ $uso -gt 80 ]; then log "disco lleno"; exit 1; fi
}Soluciones
Solución 1. Imprime reservas: 25, y es una casualidad afortunada que oculta dos errores.
Lo que ocurre: total dentro de procesar no es local, así que la función pisa la variable global del llamante; la suma 7+6+5+4+3 da 25 y esa es la que se imprime, no el 1000 asignado antes. El return "$total" es además inútil aquí y peligroso en general: si las reservas sumaran 300, return devolvería 44 (300 módulo 256) y quien lo usara obtendría un dato falso sin ningún aviso.
# Devuelve la suma por stdout, el canal correcto para un dato. 'local'
# impide pisar variables del llamante (también la 'n' del bucle, que es
# el olvido más frecuente).
procesar() {
local suma=0 n
for n in "$@"; do suma=$(( suma + n )); done
printf '%d\n' "$suma"
}
total=1000
reservas=$(procesar 7 6 5 4 3)
printf 'previo: %d reservas: %d\n' "$total" "$reservas"
# -> previo: 1000 reservas: 25Ahora total conserva su valor y el resultado llega por su canal.
Solución 2.
# resumen_casa FICHERO_CSV CASA -> "casa;reservas;noches;importe".
# Devuelve: 0 si la casa aparece en el fichero; 1 si no o si no se puede leer.
resumen_casa() {
local csv="${1:-}" casa="${2:-}" salida
[[ -r $csv ]] || { error "no puedo leer $csv"; return 1; }
salida=$(LC_ALL=C awk -F';' -v c="$casa" '
NR > 1 && $3 == c { r++; n += $5; i += $6 }
END { if (r) printf "%s;%d;%d;%.2f\n", c, r, n, i }' "$csv")
[[ -n $salida ]] || return 1
printf '%s\n' "$salida"
}
# Casos añadidos a test_comunes.sh
CSV="/home/operador/datos/reservas.csv"
comprobar "resumen mas-figueres" "mas-figueres;7;21;2450.00" \
"$(resumen_casa "$CSV" mas-figueres)"
resumen_casa "$CSV" casa-inexistente >/dev/null 2>&1
comprobar "resumen casa inexistente devuelve 1" "1" "$?"
operador@srv-tramontana:~$ ~/scripts/test_comunes.sh | tail -2
ok resumen mas-figueres
ok resumen casa inexistente devuelve 1El diseño respeta las reglas del apartado 9: el dato sale por stdout, el error por stderr, el código de retorno distingue los casos, y la función no llama a exit ni asume directorio de trabajo. Que awk no imprima nada sin coincidencias es lo que permite distinguir «no existe» de «existe con ceros».
Solución 3. Los cinco problemas:
#!/bin/shconfunction. Contradictorio: dash no conocefunction. Una librería que se carga consourcelleva shebang por convención y para los editores, pero no debe ser ejecutable.echo "cargando librería..."en el nivel superior. Ejecuta trabajo al cargarse y escribe en stdout, contaminando la salida de cualquier script que la use. Una librería carga en silencio.exit 1dentro de la librería. Mata el script del llamante sin darle opción, y si alguien la carga en su terminal, la cierra. La función debe informar y devolver un código.- Sin
local, sin guarda de inclusión y sin documentación.usoes global y pisará cualquier variable homónima, cargar el fichero dos veces redefine todo, y ninguna función dice qué devuelve. ` `,$usosin comillas yLOGFILEfijo en/tmp. Un fichero de log predecible en/tmpes un riesgo de seguridad clásico (04-06), y[ $uso -gt 80 ]falla siusoqueda vacío.
#!/usr/bin/env bash
# comunes_disco.sh - Utilidades de disco. Se carga con 'source'.
[[ -n ${TRAMONTANA_DISCO_CARGADA:-} ]] && return 0
readonly TRAMONTANA_DISCO_CARGADA=1
# uso_disco [PUNTO_MONTAJE] -> imprime el porcentaje de uso (solo el número).
# Devuelve: 1 si no se pudo medir.
uso_disco() {
local punto="${1:-/}" pct
pct=$(df --output=pcent "$punto" 2>/dev/null | tail -1 | tr -dc '0-9') || return 1
[[ -n $pct ]] || return 1
printf '%s\n' "$pct"
}
# disco_por_encima UMBRAL [PUNTO_MONTAJE]
# Devuelve: 0 si el uso supera UMBRAL, 1 si no, 2 si no se pudo medir.
disco_por_encima() {
local umbral="${1:?falta el umbral}" punto="${2:-/}" pct
pct=$(uso_disco "$punto") || { error "no puedo medir $punto"; return 2; }
(( pct > umbral ))
}Ahora quien la usa decide: disco_por_encima 80 || exit 0, o disco_por_encima 80 && purgar_releases.sh. La librería informa, el script actúa.
Conclusión
Tus scripts han dejado de repetirse, y de paso has entendido las dos rarezas de Bash que más confunden a quien viene de otros lenguajes.
- Extraes funciones por la regla de tres, porque el bloque pedía un comentario o porque había un tercer nivel de anidamiento, y las nombras para que la llamada se lea como una frase. Usas la sintaxis
nombre() { ... }, defines antes de usar, y sabes que dentro de una función$1y$@son los de la función, no los del script. - Declaras
localen todas las variables de función, y entiendes que Bash tiene ámbito dinámico: lo que declares es visible en las funciones que llames, con el acoplamiento invisible que eso implica. - Sabes que
returnsolo devuelve un código de 0 a 255 y conoces las tres formas reales de devolver datos, con stdout por defecto ylocal -nprefijado con_cuando hace falta. - Escribes funciones predicado sin
returnexplícito, aprovechando que la función devuelve el código de su última instrucción, y añadesreturn 0cuando esa última instrucción podría fallar legítimamente. Conocescommandybuiltinpara llamar al original cuando sobrescribes un nombre, y la razón para no hacerlo en una librería compartida. - Tienes
lib/comunes.shconlog(),error(),morir(),requiere_comando(),confirmar()yformatear_bytes(), cargada conSCRIPT_DIR, protegida con guarda de inclusión, documentada función a función y probada portest_comunes.sh.
Y un ejercicio de honestidad. revision_salud.sh lleva set -euo pipefail desde la primera lección y nunca has visto qué hace exactamente. log() acaba con un return 0 que ha aparecido dos veces «porque si no, el script muere», sin explicación real. informe_reservas.sh crea el directorio del informe y, si algo falla a mitad, lo deja a medio escribir. Y respaldo_tramontana.sh, prometido desde el Módulo 2, sigue sin existir. La siguiente lección, Depuración y Manejo de Errores, cierra todo eso: bash -x con un PS4 legible, los códigos de shellcheck que más importan, el modo estricto desmenuzado —incluidos los cinco sitios donde set -e no funciona—, trap con EXIT, ERR e INT para que ningún temporal ni ningún bloqueo sobreviva a una interrupción, la idempotencia como objetivo de diseño, y por fin el script de respaldo completo y blindado.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
