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:
| Canal | Descriptor | Para | Ejemplo |
|---|---|---|---|
| Salida estándar | 1 | El 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 errores | 2 | Todo 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 pobre | Mensaje útil |
|---|---|
Error! | respaldo: error: no existe el directorio /mnt/usb |
Falló la copia | respaldo: error: rsync terminó con código 23 (copia parcial); revisa los permisos de /var/www |
Argumento inválido | respaldo: -d debe ser un número de días, no «treinta» |
No hay espacio | respaldo: error: /mnt/usb tiene 120 MiB libres y el respaldo necesita 2.3 GiB |
- Quién: el nombre del script al comienzo. Cuando varios scripts escriben en el mismo registro, o un script llama a otro, es la única forma de saber de dónde viene cada línea.
- Qué: en términos del problema, no del código. «No existe el directorio» sirve; «falló la línea 42» no, salvo para quien depura.
- Con qué dato: la ruta, el valor, el código de salida. Es lo primero que se necesita para investigar.
- Qué hacer: cuando la solución es evidente para el script, decirla ahorra tiempo: «revisa los permisos», «prueba con -h».
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:
| Nivel | Para | Ejemplo |
|---|---|---|
DEBUG | Detalle interno, útil solo para quien investiga un problema. | usando rsync con: -a --delete --exclude .cache/ |
INFO | Lo que el script hizo, en condiciones normales. | respaldo creado: web-2026-10-14.tgz (48 MiB) |
AVISO | Algo anómalo que no impidió el trabajo, pero merece atención. | el disco de respaldos está al 87 % |
ERROR | Algo 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:
# 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.
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 %
- Los niveles se comparan por peso, con dos arrays asociativos del tema 8: un mensaje se muestra si su peso es mayor o igual que el del nivel mínimo.
${LOG_NIVEL^^}acepta el nivel escrito en minúsculas, y un nivel desconocido se trata comoINFO. printf -vcon%(…)Tobtiene la fecha sin crear un proceso, como vimos en el tema 19. En un script que registra mucho, la diferencia con$(date …)se nota.- El número de proceso,
$$, distingue las líneas de dos ejecuciones simultáneas del mismo script en un registro compartido. - La función termina con
return 0. Sin esa línea, cuando el archivo no está configurado, la última prueba daría falso, y conset -euna simple llamada alog_infodetendría el script. Es el caso del tema 23: el código de una función es el de su último comando. - Las variables de configuración tienen valores por defecto con
: "${…:=…}", del tema 6: un script puede fijarlas antes de cargar la biblioteca, y también cambiarlas después, porque se leen en cada llamada.
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:
[[ -t 2 ]]: la salida de errores es una terminal, como vimos con-t 0en el tema 9. Si está redirigida a un archivo o a una tubería, no hay colores.NO_COLORno está definida. Es una convención compartida por muchos programas: quien prefiere no ver colores defineNO_COLOR=1en su entorno, y todos los programas que la respetan dejan de usarlos.
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:
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 script | Dó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 systemd | Ningú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:
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:
$ 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:
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:
- Contraseñas, claves y tokens. Ni completos ni en parte. Cuidado con registrar comandos enteros, como una URL que lleva una clave como parámetro, o las opciones de un comando que incluyen una contraseña. Y cuidado con
set -x, que muestra cada línea ejecutada con las variables ya expandidas. - Datos personales que no hacen falta para entender el problema.
- Volúmenes desproporcionados. Una línea por cada archivo procesado de un millón llena el disco y esconde lo importante. El detalle va en
DEBUG, y enINFOun resumen: «procesados 1 000 000 archivos, 3 con errores».
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.
#!/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 ))
$ 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 %
- Cada nivel tiene su papel: los discos normales solo se mencionan en
DEBUG; los que se acercan al límite, enAVISO; los críticos, enERROR; y el resumen, enINFO. - Con
-q, en una tarea programada, el script no escribe nada si todo está bien, y escribe solo los errores si no. El código de salida permite además que otro script reaccione, por ejemplo enviando un aviso. - La configuración llega por el entorno: los umbrales del script y las variables de la biblioteca, como
LOG_ARCHIVO, sin necesidad de más opciones. -x tmpfs -x devtmpfsexcluye los sistemas de archivos en memoria, que no ocupan disco y cuyo uso no interesa aquí.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
echo "Procesando…" en un script que produce datos | Los 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 siempre | Secuencias de escape en los archivos y correos. | Solo con [[ -t 2 ]] y sin NO_COLOR. |
Una función de registro sin return 0 | Con set -e, un mensaje filtrado detiene el script. | return 0 al final. |
$(date) en cada línea de registro | Un proceso por mensaje. | printf -v fecha '%(…)T' -1 |
Mensajes de éxito en una tarea de cron | Un correo diario que nadie lee. | Nivel ERROR con -q. |
| Registrar el comando completo con su contraseña | El secreto queda guardado y se copia. | Registrar sin los datos sensibles. |
| Un archivo de registro que solo crece | Termina llenando el disco. | Rotarlo, o usar el registro del sistema. |
13. Resumen
- La salida estándar es para los datos; la de errores, para todo mensaje dirigido a personas.
- Un buen mensaje dice quién habla, qué pasó, con qué dato y, si se puede, qué hacer.
- Los niveles
DEBUG,INFO,AVISOyERRORclasifican los mensajes; un nivel mínimo decide cuáles se muestran. - Una biblioteca de registro da a todos los scripts un formato común: fecha, script, proceso y nivel.
- Los colores se usan solo si la salida es una terminal y
NO_COLORno está definida. - Los mensajes pueden guardarse en un archivo, que conviene rotar.
exec >> registro 2>&1redirige toda la salida del script, incluida la de los comandos que ejecuta; contee, también se ve en pantalla.loggerescribe en el registro del sistema, que se consulta conjournalctl -t.-vy-qajustan el nivel; encron, un script silencioso en el éxito solo produce correo cuando hay algo que leer.- Nunca se registran contraseñas ni datos innecesarios, ni volúmenes que entierren lo importante.
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
- Toma un script de un tema anterior y reescribe sus mensajes de error para que respondan las cuatro preguntas de la sección 2.
- Crea
lib/registro.shen tu proyecto de herramientas y pruébala con los cuatro niveles y distintos valores deLOG_NIVEL. - Comprueba que con
revisar-discos 2> salida.txtel archivo no tiene secuencias de color, concat -A. - Escribe un script que use
exec > >(tee -a registro.log) 2>&1y ejecute un comando que falle. Comprueba que el error del comando quedó en el registro. - Si tienes systemd, envía un mensaje con
loggery encuéntralo conjournalctl -t. - Convierte
respaldardel tema 18 para que use la biblioteca de registro, con un archivo en~/.local/state, y agrégale-vy-q.