← Automatización con Scripts en Bash Bloque 6 · Manejo de errores y scripts robustos · Tema 25 de 42

Tema 25

Mensajes de error y registros

Un script que se ejecuta a mano habla con una persona que lo está mirando. Uno que se ejecuta de madrugada desde cron habla con alguien que leerá sus mensajes horas o días después, cuando algo ya salió mal. Los dos necesitan mensajes claros, pero el segundo necesita además que queden guardados, con la fecha, y con una indicación de su gravedad. Este tema reúne todo lo que el curso dijo sobre mensajes en una biblioteca de registro reutilizable.

1. Dos canales, dos propósitos

Todo proceso tiene dos salidas, como vimos en el curso de Terminal, y la regla que el curso aplica desde el tema 4 se basa en ellas:

CanalDescriptorParaEjemplo
Salida estándar1El resultado: los datos que el script produce y que otro programa podría procesar.La lista de archivos encontrados, un informe, un valor.
Salida de errores2Todo lo demás: errores, avisos, progreso, diagnóstico.«No existe el directorio», «Procesando 3 de 10».

El nombre «salida de errores» es engañoso: en realidad es el canal de los mensajes para personas, sean o no errores. La separación permite que quien usa el script redirija cada cosa a donde corresponde. Con buscar-grandes > lista.txt, la lista va al archivo y los avisos siguen en la pantalla; con respaldo 2>> respaldo.log, los mensajes van al registro y el resultado sigue fluyendo por la tubería.

Si un script escribe sus mensajes en la salida estándar, rompe esa posibilidad: cualquiera que capture su resultado con $( ) o lo conecte a otro comando recibirá los mensajes mezclados con los datos, como vimos en el tema 19.

2. Cómo es un buen mensaje

Un mensaje de error lo lee alguien que no sabe qué pasó, a menudo sin acceso al código. Para que sirva, debe responder cuatro preguntas: quién lo dice, qué pasó, con qué dato y, si se puede, qué hacer.

Mensaje pobreMensaje útil
Error!respaldo: error: no existe el directorio /mnt/usb
Falló la copiarespaldo: error: rsync terminó con código 23 (copia parcial); revisa los permisos de /var/www
Argumento inválidorespaldo: -d debe ser un número de días, no «treinta»
No hay espaciorespaldo: error: /mnt/usb tiene 120 MiB libres y el respaldo necesita 2.3 GiB

3. Niveles de gravedad

No todos los mensajes tienen la misma importancia. Clasificarlos por nivel permite dos cosas: que quien lee el registro encuentre rápido lo grave, y que el script muestre más o menos detalle según se necesite. Casi todos los sistemas de registro usan una escala parecida a esta:

NivelParaEjemplo
DEBUGDetalle interno, útil solo para quien investiga un problema.usando rsync con: -a --delete --exclude .cache/
INFOLo que el script hizo, en condiciones normales.respaldo creado: web-2026-10-14.tgz (48 MiB)
AVISOAlgo anómalo que no impidió el trabajo, pero merece atención.el disco de respaldos está al 87 %
ERRORAlgo que impidió hacer lo pedido, en todo o en parte.no se pudo conectar con db01

El script se configura con un nivel mínimo: se muestran los mensajes de ese nivel y los más graves. Con INFO, el uso normal, se ven INFO, AVISO y ERROR; con DEBUG, todo; con ERROR, solo los errores, que es lo que conviene para una tarea programada que no debe escribir nada si todo sale bien.

4. Una biblioteca de registro

Siguiendo el tema 22, las funciones de registro van en una biblioteca, para que todos los scripts del proyecto escriban sus mensajes con el mismo formato. Esta es la del curso:

lib/registro.sh
# registro.sh — funciones de registro con niveles, fecha y, opcionalmente, archivo y syslog.
#
# Configuración, con variables que el script puede fijar antes o después de cargarla:
#   LOG_NIVEL    nivel mínimo que se muestra: DEBUG, INFO, AVISO o ERROR (INFO)
#   LOG_ARCHIVO  si tiene valor, cada mensaje se agrega también a ese archivo
#   LOG_SYSLOG   1 para enviar además los mensajes al registro del sistema
#
# Uso: log_debug, log_info, log_aviso, log_error MENSAJE...; log_morir MENSAJE...

[[ -n "${_REGISTRO_SH:-}" ]] && return 0
_REGISTRO_SH=1

: "${LOG_NIVEL:=INFO}"
: "${LOG_ARCHIVO:=}"
: "${LOG_SYSLOG:=0}"

declare -gA _LOG_PESO=([DEBUG]=10 [INFO]=20 [AVISO]=30 [ERROR]=40)
declare -gA _LOG_COLOR=([DEBUG]=$'\e[2m' [INFO]='' [AVISO]=$'\e[33m' [ERROR]=$'\e[31m')
declare -gA _LOG_SYSLOG=([DEBUG]=debug [INFO]=info [AVISO]=warning [ERROR]=err)

_log() {
  local nivel="$1"
  shift
  local minimo="${LOG_NIVEL^^}"
  (( ${_LOG_PESO[$nivel]} >= ${_LOG_PESO[$minimo]:-20} )) || return 0

  local fecha linea
  printf -v fecha '%(%Y-%m-%d %H:%M:%S)T' -1
  linea="$fecha ${0##*/}[$$] $nivel: $*"

  if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then
    printf '%s%s\e[0m\n' "${_LOG_COLOR[$nivel]}" "$linea" >&2
  else
    printf '%s\n' "$linea" >&2
  fi

  if [[ -n "$LOG_ARCHIVO" ]]; then
    printf '%s\n' "$linea" >> "$LOG_ARCHIVO"
  fi
  if [[ "$LOG_SYSLOG" == 1 ]] && command -v logger > /dev/null; then
    logger -t "${0##*/}" -p "user.${_LOG_SYSLOG[$nivel]}" -- "$*"
  fi
  return 0
}

log_debug() { _log DEBUG "$@"; }
log_info()  { _log INFO "$@"; }
log_aviso() { _log AVISO "$@"; }
log_error() { _log ERROR "$@"; }

log_morir() {
  _log ERROR "$@"
  exit 1
}

Cada línea de registro tiene un formato fijo, fácil de leer y de filtrar con grep: fecha y hora, nombre del script, número de proceso entre corchetes y nivel.

el formato de cada línea
2026-10-14 03:15:02 respaldo[48213] INFO: respaldo creado: web-2026-10-14.tgz (48.3 MiB)
2026-10-14 03:15:02 respaldo[48213] AVISO: el disco de respaldos está al 87 %

5. Colores, solo en la terminal

En la terminal, un aviso en amarillo y un error en rojo se ven de inmediato entre muchas líneas. Los colores se producen con secuencias de escape: caracteres especiales que la terminal interpreta como órdenes en lugar de mostrarlos. $'\e[31m' activa el rojo y $'\e[0m' vuelve a lo normal.

El problema es que esas secuencias, en un archivo o en un correo, aparecen como basura: ^[[31m2026-10-14 … ERROR. Por eso la biblioteca solo usa colores si se cumplen dos condiciones:

6. Guardar los mensajes en un archivo

Un script que corre solo debe dejar constancia de lo que hizo. Con la biblioteca, basta con indicar un archivo en LOG_ARCHIVO, y cada mensaje se agrega, sin colores, además de mostrarse:

registrar también en un archivo
LOG_ARCHIVO="${LOG_ARCHIVO:-$HOME/.local/state/respaldo.log}"
mkdir -p -- "$(dirname "$LOG_ARCHIVO")"
source "$DIR_BASE/lib/registro.sh"

¿Dónde guardar el registro? Las convenciones de Linux sugieren:

Quién ejecuta el scriptDónde
Un usuario, para sí mismo~/.local/state/ (el estándar actual) o un directorio propio, como ~/logs/.
El sistema, como root/var/log/, en un archivo o un directorio con el nombre del script.
Un servicio de systemdNingún archivo: su salida va al journal automáticamente (sección 8).

Un registro que solo crece termina llenando el disco. Para los registros propios, el script rotar-registros del tema 14 o la herramienta logrotate del sistema se encargan de rotarlos.

7. Redirigir toda la salida del script

La biblioteca registra los mensajes propios del script, pero no lo que escriben los comandos que ejecuta, como los errores de rsync o tar. Para capturar todo, el comando exec sin otro comando cambia las redirecciones del propio script, desde ese punto hasta el final:

dos formas de redirigir todo
exec >> "$registro" 2>&1                    # todo al archivo; nada a la pantalla

exec > >(tee -a "$registro") 2>&1           # todo al archivo y también a la pantalla

La primera forma es la habitual en scripts que solo corren desde cron: desde esa línea, la salida estándar y la de errores van al registro, incluida la de todos los comandos. La segunda usa una sustitución de procesos, como las del tema 16, para pasar todo por tee, que escribe en el archivo y en la pantalla a la vez.

Las dos tienen un costo: los datos y los mensajes quedan mezclados en el mismo lugar. Para un script cuyo único resultado son mensajes, como un respaldo, no importa. Para uno que produce datos, conviene redirigir solo la salida de errores: exec 2>> "$registro".

8. El registro del sistema: logger

Linux tiene un registro central donde los programas y servicios dejan sus mensajes: el journal de systemd, o los archivos de /var/log/syslog en sistemas más antiguos. El comando logger escribe un mensaje en él, con una etiqueta y una prioridad:

terminal
$ logger -t respaldo -p user.warning "el disco de respaldos está al 87 %"
$ journalctl -t respaldo --since today
oct 14 03:15:02 equipo respaldo[48213]: el disco de respaldos está al 87 %
$ journalctl -t respaldo -p warning          # solo avisos y errores

Usar el registro del sistema tiene ventajas: no hay archivos propios que crear ni rotar, los mensajes de todos los programas quedan en un solo lugar con la hora exacta, y las herramientas de monitoreo ya saben leerlo. Por eso la biblioteca ofrece la opción LOG_SYSLOG=1. Las prioridades de syslog equivalentes a los niveles del curso son debug, info, warning y err.

9. Más o menos detalle: -v y -q

Como vimos en el tema 20, -v y -q son las opciones convencionales para pedir más o menos detalle. Con la biblioteca, se traducen directamente al nivel mínimo:

opciones de verbosidad
while getopts ":vq" opcion; do
  case "$opcion" in
    v) LOG_NIVEL=DEBUG ;;           # todo, para investigar
    q) LOG_NIVEL=ERROR ;;           # solo errores, para cron
    *) uso ;;
  esac
done

La salida silenciosa en el uso normal es especialmente valiosa con cron, que por defecto envía por correo todo lo que una tarea escribe. Un script que en cada ejecución escribe «todo bien» genera un correo diario que nadie lee, y el día que hay un error, nadie lo ve. Con -q, solo llega correo cuando hay algo que leer. Lo veremos en el tema 34.

10. Qué no registrar

Un registro se guarda, se copia, se envía por correo y a veces se comparte para pedir ayuda. Hay cosas que nunca deben estar en él:

11. Un script completo

Este script revisa el uso de todos los sistemas de archivos montados y registra un aviso o un error según umbrales configurables. Usa la biblioteca de registro, admite -v y -q, y termina con código 1 si algún disco está en estado crítico, para que pueda usarse desde cron o desde un sistema de monitoreo.

bin/revisar-discos
#!/usr/bin/env bash
#
# revisar-discos — avisa cuando un sistema de archivos supera un umbral de uso.
#
# Uso:     revisar-discos [-v | -q]
# Entorno: UMBRAL_AVISO (80), UMBRAL_ERROR (95), y los de lib/registro.sh
# Salida:  0 si ningún sistema de archivos supera UMBRAL_ERROR; 1 si alguno lo supera.
#
set -euo pipefail

readonly DIR_BASE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
# shellcheck source=../lib/registro.sh
source "$DIR_BASE/lib/registro.sh"

readonly UMBRAL_AVISO="${UMBRAL_AVISO:-80}"
readonly UMBRAL_ERROR="${UMBRAL_ERROR:-95}"

while getopts ":vq" opcion; do
  case "$opcion" in
    v) LOG_NIVEL=DEBUG ;;
    q) LOG_NIVEL=ERROR ;;
    *) echo "Uso: ${0##*/} [-v | -q]" >&2; exit 2 ;;
  esac
done

criticos=0
while read -r punto uso; do
  uso="${uso%\%}"                             # «84%» → «84»
  if (( uso >= UMBRAL_ERROR )); then
    log_error "$punto está al $uso %"
    criticos=$(( criticos + 1 ))
  elif (( uso >= UMBRAL_AVISO )); then
    log_aviso "$punto está al $uso %"
  else
    log_debug "$punto está al $uso %"
  fi
done < <(df --output=target,pcent -x tmpfs -x devtmpfs | tail -n +2)

log_info "revisión terminada: $criticos sistemas de archivos críticos"
(( criticos == 0 ))
terminal
$ revisar-discos
2026-10-14 12:46:18 revisar-discos[806] AVISO: / está al 84 %
2026-10-14 12:46:18 revisar-discos[806] INFO: revisión terminada: 0 sistemas de archivos críticos
$ revisar-discos -v
2026-10-14 12:46:20 revisar-discos[813] AVISO: / está al 84 %
2026-10-14 12:46:20 revisar-discos[813] DEBUG: /boot/efi está al 2 %
2026-10-14 12:46:20 revisar-discos[813] DEBUG: /home está al 46 %
2026-10-14 12:46:20 revisar-discos[813] INFO: revisión terminada: 0 sistemas de archivos críticos
$ UMBRAL_ERROR=80 LOG_ARCHIVO=/tmp/discos.log revisar-discos -q; echo "código $?"
2026-10-14 12:46:25 revisar-discos[820] ERROR: / está al 84 %
código 1
$ cat /tmp/discos.log
2026-10-14 12:46:25 revisar-discos[820] ERROR: / está al 84 %

12. Errores frecuentes

EscribisteQué pasaCorrección
echo "Procesando…" en un script que produce datosLos mensajes se mezclan con los datos.Los mensajes a >&2.
echo "Error"No dice quién, qué ni con qué dato.script: error: no existe /mnt/usb
Colores siempreSecuencias de escape en los archivos y correos.Solo con [[ -t 2 ]] y sin NO_COLOR.
Una función de registro sin return 0Con set -e, un mensaje filtrado detiene el script.return 0 al final.
$(date) en cada línea de registroUn proceso por mensaje.printf -v fecha '%(…)T' -1
Mensajes de éxito en una tarea de cronUn correo diario que nadie lee.Nivel ERROR con -q.
Registrar el comando completo con su contraseñaEl secreto queda guardado y se copia.Registrar sin los datos sensibles.
Un archivo de registro que solo creceTermina llenando el disco.Rotarlo, o usar el registro del sistema.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué un mensaje de progreso debe ir a la salida de errores aunque no sea un error?

Porque la salida estándar es el canal de los datos. Si el mensaje fuera allí, se mezclaría con el resultado cuando la salida se redirige, se captura con $( ) o se conecta a otro comando.

2. ¿Qué cuatro preguntas debería responder un mensaje de error?

Quién lo dice (el nombre del script), qué pasó, con qué dato (la ruta, el valor, el código) y, si es posible, qué hacer para resolverlo.

3. Con el nivel mínimo en AVISO, ¿qué mensajes se muestran?

Los de nivel AVISO y ERROR. DEBUG e INFO se omiten.

4. ¿Por qué la función _log termina con return 0?

Porque su código sería el de su último comando, una prueba que puede ser falsa. Con set -e, una llamada a log_info que devolviera 1 detendría el script.

5. ¿Qué condiciones deben cumplirse para usar colores en los mensajes?

Que la salida de errores sea una terminal ([[ -t 2 ]]) y que la variable NO_COLOR no esté definida.

6. ¿Qué hace exec >> registro.log 2>&1 en medio de un script?

Redirige la salida estándar y la de errores del script, desde ese punto hasta el final, al archivo, agregando. Incluye lo que escriban todos los comandos que el script ejecute después.

7. ¿Qué ventaja tiene exec > >(tee -a registro.log) 2>&1 sobre la forma anterior?

Que todo se escribe en el archivo y además se sigue viendo en la pantalla.

8. ¿Cómo se envía un aviso al registro del sistema y cómo se lo consulta?

Con logger -t etiqueta -p user.warning "mensaje", y se consulta con journalctl -t etiqueta.

9. ¿Por qué conviene que una tarea de cron no escriba nada cuando todo sale bien?

Porque cron envía por correo lo que la tarea escribe. Si escribe en cada ejecución, el correo se vuelve rutina y nadie lo lee; si solo escribe ante un problema, cada correo significa algo.

10. ¿Por qué set -x es un riesgo en un script que maneja contraseñas?

Porque muestra cada línea ejecutada con las variables ya expandidas, incluida la contraseña, y si la salida de errores se guarda en un registro, el secreto queda escrito.

Para practicar

  1. Toma un script de un tema anterior y reescribe sus mensajes de error para que respondan las cuatro preguntas de la sección 2.
  2. Crea lib/registro.sh en tu proyecto de herramientas y pruébala con los cuatro niveles y distintos valores de LOG_NIVEL.
  3. Comprueba que con revisar-discos 2> salida.txt el archivo no tiene secuencias de color, con cat -A.
  4. Escribe un script que use exec > >(tee -a registro.log) 2>&1 y ejecute un comando que falle. Comprueba que el error del comando quedó en el registro.
  5. Si tienes systemd, envía un mensaje con logger y encuéntralo con journalctl -t.
  6. Convierte respaldar del tema 18 para que use la biblioteca de registro, con un archivo en ~/.local/state, y agrégale -v y -q.