Depuración en Bash: técnicas prácticas con set -x, set -v y bashdb

DUGLAS MORENODUGLAS MORENO 👁 25

Introducción

Cuando escribís scripts en Bash, es común que aparezcan errores sutiles: variables sin inicializar, comandos que fallan silenciosamente o lógica que no se ejecuta como esperabas. Depurar estos problemas puede resultar frustrante si solo recurrís a echo esporádicos. Afortunadamente, Bash ofrece herramientas integradas que permiten ver exactamente qué está ocurriendo en cada paso del script, además de depuradores externos como bashdb que brindan una experiencia más cercana a la de un IDE.

En este tutorial vas a aprender:

  • Cómo activar el rastreo de ejecución con set -x y qué información muestra.
  • Cómo usar set -v para ver cada línea tal como es leída por el intérprete.
  • Cómo combinar ambas opciones para obtener un diagnóstico más completo.
  • Cómo crear un depurador simple usando la señal DEBUG y un trap.
  • Una introducción rápida a bashdb, su instalación básica y comandos esenciales.
  • Ejemplos concretos de salida de depuración al ejecutar un script que contiene errores intencionales.
  • Buenas prácticas y errores frecuentes que evitás al depurar en Bash.

El contenido está pensado para desarrolladores, administradores de sistemas y cualquier persona que escriba o mantenga scripts de shell y quiera ganar confianza al diagnosticar problemas sin depender únicamente de adivinanzas.


Habilitando el rastreo con set -x

La opción -x hace que Bash muestre cada comando antes de ejecutarlo, expandiendo variables y sustituiciones. Cada línea de salida comienza con el carácter + (o con el valor de la variable PS4 si la cambiás). Esto es invaluable para ver exactamente qué se está ejecutando, especialmente cuando las variables cambian de valor inesperadamente.

Ejemplo básico

Creemos un pequeño script que asigna una variable, la imita y luego ejecuta un comando que falla:

#!/usr/bin/env bash
set -x
echo "Inicio del script"
mi_var=42
echo "El valor de mi_var es: $mi_var"
false   # este comando devuelve un código de error
echo "Esta línea no se alcanzará"

Al ejecutarlo, la salida será algo como:

+ echo 'Inicio del script'
Inicio del script
+ mi_var=42
+ echo 'El valor de mi_var es: 42'
El valor de mi_var es: 42
+ false
+ echo 'Esta línea no se alcanzará'

Observá que, pese a que false falla, el script continúa porque no hemos activado set -e. Si quisieramos que el script se detuviera ante el primer error, agregaríamos set -e antes o después de set -x.

Personalizando el prefijo de rastreo

La variable PS4 controla el prefijo que se muestra antes de cada línea de rastreo. Por defecto es + , pero podés cambiarla a cualquier cadena, por ejemplo incluir el número de línea o el nombre del script:

#!/usr/bin/env bash
PS4='+\[$BASH_SOURCE:$LINENO\] '
set -x
echo "Personalizando PS4"

Esto produciría algo como:

+[script.sh:5] echo 'Personalizando PS4'
Personalizando PS4

Esto es útil cuando tenés scripts largos y necesitás ubicar rápidamente el origen de cada comando.


Mostrando las entradas con set -v

Mientras que set -x muestra los comandos después de que Bash los ha expandido, set -v (verbose) muestra cada línea del script tal como es leída, antes de cualquier sustitución. Esto permite ver el código fuente tal cual estuvo escrito, lo que ayuda a detectar errores de sintaxis o de comillas que pueden quedar ocultos tras la expansión.

Ejemplo de set -v

#!/usr/bin/env bash
set -v
# Esta es una comentario
mi_var="Hola, mundo"
echo $mi_var
# Intencionalmente dejamos una variable sin cerrar para ver el efecto
mal_var="Esto está sin cerrar
echo $mal_var

Al correrlo, la salida incluirá las líneas tal como aparecen en el archivo:

#!/usr/bin/env bash
set -v
# Esta es una comentario
mi_var="Hola, mundo"
echo $mi_var
# Intencionalmente dejamos una variable sin cerrar para ver el efecto
mal_var="Esto está sin cerrar

Luego Bash intentará ejecutar el comando echo $mi_var y mostrará su salida, seguido del error de sintaxis producido por la comilla faltante. De esta manera podés ver exactamente dónde se rompe la lectura del script.

Combinando set -x y set -v

Usar ambas opciones simultáneamente brinda una vista doble: primero ves la línea tal como está escrita (-v) y luego, después de la expansión, el comando que realmente se ejecuta (-x).

#!/usr/bin/env bash
set -xv
saludo="¡Hola, $USER!"
echo $saludo

Salida esperada:

+ set -xv
+ saludo="¡Hola, tuusuario!"
+ echo '¡Hola, tuusuario!'
¡Hola, tuusuario!

En la primera línea vemos la asignación tal como fue leída (por -v), luego la expansión de $USER (por -x).


Un depurador simple con trap DEBUG

Bash permite ejecutar una acción cada vez que está a punto de ejecutarse un comando mediante la señal DEBUG. Al combinarla con trap, podemos imprimir información útil como el número de línea, el comando que se va a ejecutar y el valor de ciertas variables.

Implementando un depurador básico

#!/usr/bin/env bash
# Función que se ejecutará antes de cada comando
debug_step() {
    echo "[DEBUG] Línea: $LINENO | Comando: '$BASH_COMMAND'"
}
# Asociamos la función a la señal DEBUG
trap 'debug_step' DEBUG

# Ahora el script que queremos observar
print_var() {
    local texto="Valor interno"
    echo $texto
}

print_var
unset no_existe  # esto generará un mensaje de error pero continuamos
echo "Fin"

La salida será algo como:

[DEBUG] Línea: 6 | Comando: 'print_var'
[DEBUG] Línea: 9 | Comando: 'local texto="Valor interno"'
[DEBUG] Línea: 10 | Comando: 'echo $texto'
Valor interno
[DEBUG] Línea: 12 | Comando: 'unset no_existe'
[DEBUG] Línea: 13 | Comando: 'echo "Fin"'
Fin

Este enfoque es ligero y no requiere instalar nada extra. Podés ampliar la función debug_step para mostrar el contenido de variables específicas, el valor de $? o incluso escribir en un archivo de log.

Limitaciones

  • El trap DEBUG se dispara también para comandos internos de Bash como for, if o function. Si necesitás filtrarlos, podés comprobar $BASH_COMMAND dentro de la función.
  • No se muestra el estado de expansión de variables antes de la ejecución; para eso seguís necesitando set -x.

Introducción a bashdb

bashdb es un depurador externo escrito en Bash que ofrece puntos de interrupción, paso a paso, inspección de variables y pila de llamadas, semejante a lo que ofrecen gdb o pdb en otros lenguajes. No viene instalado por defecto en la mayoría de distribuciones, pero su instalación es sencilla.

Instalación rápida

En sistemas basados en Debian/Ubuntu:

sudo apt-get update
sudo apt-get install bashdb

En sistemas con dnf (Fedora, RHEL):

sudo dnf install bashdb

En macOS con Homebrew:

brew install bashdb

Una vez instalado, podés invocar el depurador sobre cualquier script de Bash:

bashdb mi_script.sh

Esto lanzará una interfaz similar a la de gdb donde podés usar comandos como:

  • break o b para establecer un punto de interrupción en una línea o función.
  • continue o c para ejecutar hasta el siguiente punto de interrupción.
  • step o s para ejecutar la siguiente instrucción y detenerse.
  • print o p para mostrar el valor de una variable.
  • backtrace o bt para ver la pila de llamadas.
  • quit o q para salir.

Sesión de ejemplo con bashdb

Supongamos el siguiente script con un error lógico:

#!/usr/bin/env bash
suma() {
    local a=$1
    local b=$2
    echo $(( a + b ))
}

resultado=$(suma 5 7)
echo "El resultado es: $resultado"
# Intentamos usar la función con un parámetro faltante
resultado2=$(suma 3)
echo "Resultado con un solo parámetro: $resultado2"

Al ejecutarlo normalmente, obtendríamos:

El resultado es: 12
bash: line 6: 3 + : sintaxis error: operando esperado (error token is "+" )
Resultado con un solo parámetro: 

El segundo llamado a suma falla porque la variable b queda vacía, provocando una expresión aritmética inválida. Con bashdb podríamos detenernos justo antes de la llamada problemática e inspeccionar los valores de a y b.

Una sesión típica sería:

$ bashdb script.sh
(gdb) break 12   # establece un punto en la línea donde llamamos a suma con un parámetro
(gdb) run
... ejecución hasta el punto de interrupción ...
(gdb) print a
$1 = 3
(gdb) print b
$2 = ""
(gdb) step
... ejecuta la línea y muestra el error ...
(gdb) quit

Esto muestra cómo bashdb permite aislar el estado exacto de las variables en el momento del fallo, algo que resulta mucho más difícil de lograr con solo echo o set -x.


Ejemplos de salida de depuración al ejecutar un script con errores

A continuación, vamos a crear un script que combine varios fallos típicos: variable sin inicializar, uso incorrecto de comillas, y un comando que devuelve error de salida. Luego lo ejecutaremos con distintas opciones de depuración para observar la salida.

Script de ejemplo

Guarda el siguiente contenido en depurar_errores.sh:

#!/usr/bin/env bash
# Activamos tanto rastreo como verbose para ver todo
set -xv

echo "Inicio de la depuración"

# Variable que no hemos inicializado todavía
echo "Valor de var_sin_init: $var_sin_init"

# Asignación con comillas simples que impiden expansión
mensaje='El valor de USER es: $USER'
echo "$mensaje"

# Intentamos dividir por cero usando let (generará error)
let resultado=10/0
echo "Resultado: $resultado"

# Llamamos a un comando que no existe
comando_inexistente

echo "Fin del script"

Ejecutando solo con set -x

#!/usr/bin/env bash
set -x
source depurar_errores.sh

Salida (fragmento representativo):

+ source depurar_errores.sh
++ echo 'Inicio de la depuración'
Inicio de la depuración
++ echo 'Valor de var_sin_init: '
Valor de var_sin_init: 
++ mensaje='El valor de USER es: $USER'
++ echo 'El valor de USER es: $USER'
El valor de USER es: $USER
++ let resultado=10/0
depurar_errores.sh: línea 9: let: división por cero (error token is "0" )
++ echo 'Resultado: '
Resultado: 
++ comando_inexistente
depurar_errores.sh: línea 13: comando_inexistente: no se encontró la orden
++ echo 'Fin del script'
Fin del script

Observá cómo cada línea comienza con + y vemos la expansión de variables y el fallo de let y del comando inexistente.

Ejecutando solo con set -v

#!/usr/bin/env bash
set -v
source depurar_errores.sh

Salida (fragmento):

#!/usr/bin/env bash
# Activamos tanto rastreo como verbose para ver todo
set -xv

echo "Inicio de la depuración"

# Variable que no hemos inicializado todavía
echo "Valor de var_sin_init: $var_sin_init"

# Asignación con comillas simples que impiden expansión
mensaje='El valor de USER es: $USER'
echo "$mensaje"

# Intentamos dividir por cero usando let (generará error)
let resultado=10/0
echo "Resultado: $resultado"

# Llamamos a un comando que no existe
comando_inexistente

echo "Fin del script"

Luego Bash continúa ejecutando y muestra los mensajes de error generados por los comandos fallidos, pero no muestra la expansión de comandos porque no tuvimos -x.

Ejecutando con ambas opciones (set -xv dentro del script)

Como el propio script ya tiene set -xv, basta con ejecutarlo directamente:

./depurar_errores.sh

La salida combina lo visto anteriormente: primero la lectura de cada línea (por -v) y luego la ejecución con expansión (por -x). Esto permite correlacionar exactamente qué parte del código fuente produjo cada mensaje de error.

Usando el depurador basado en trap DEBUG

Creemos una versión del script que incluya nuestro depurador simple:

#!/usr/bin/env bash
# Depurador simple
debug_step() {
    echo "[DBG] L$LINENO: $BASH_COMMAND"
}
trap 'debug_step' DEBUG

set -e  # detenemos ante el primer error para que la salida sea más legible

echo "Inicio"
var_sin_init=""  # ahora sí la inicializamos para evitar expansión vacía
echo "var_sin_init=$var_sin_init"

mensaje='El valor de USER es: $USER'
echo "$mensaje"

let resultado=10/0  # este comando provocará la detención por set -e
"Nunca se alcanzará"

Al ejecutarlo, la salida será algo como:

[DBG] L6: echo 'Inicio'
Inicio
[DBG] L9: var_sin_init=""
[DBG] L10: echo 'var_sin_init='
var_sin_init=
[DBG] L12: mensaje='El valor de USER es: $USER'
[DBG] L13: echo 'El valor de USER es: $USER'
El valor de USER es: $USER
[DBG] L15: let resultado=10/0
depurar_errores_trap.sh: línea 15: let: división por cero (error token is "0" )

Observá que, gracias a set -e, el script se detiene justo después de la línea que falla, y el trap nos mostró cada comando antes de su ejecución, incluida la línea fallida.

Sesión corta con bashdb

Si tuvieras bashdb instalado, podrías lanzar:

bashdb depurar_errores.sh

Y dentro del intérprete de bashdb ejecutar comandos como:

(gdb) break 15   # detención en la línea de let
(gdb) run
(gdb) print resultado   # mostrará que la variable aún no tiene valor no está asignada
(gdb) continue   # finalizará mostrando el error de división por cero
(gdb) quit

Esto ilustra cómo, con un depurador completo, podés inspeccionar el estado del programa en el momento exacto del fallo, algo que resulta muy útil cuando el error depende de valores calculados previamente.


Buenas prácticas y errores comunes al depurar en Bash

Buenas prácticas

  1. Activar set -euo pipefail en entornos de desarrollo

    • -e hace que el script abandone la ejecución ante el primer comando que devuelva un código distinto de cero.
    • -u trata el uso de variables sin inicializar como error.
    • -o pipefail hace que el valor de retorno de una canalización sea el del último comando que falló, no solo del último.
      Estas opciones facilitan la detección temprana de problemas.
  2. Usar PS4 para incluir contexto
    Personalizar PS4 con el nombre del script y el número de línea (como vimos) reduce el tiempo de búsqueda en scripts extensos.

  3. Separar la lógica en funciones
    Depurar funciones individuales es más sencillo porque el alcance de las variables está limitado y los traces son más legibles.

  4. Registrar la salida de depuración en un archivo
    Si la consola se vuelve muy ruidosa, redirigí la salida de set -x a un archivo:

    exec 3>&2 2>depuracion.log
    set -x
    # ... código ...
    exec 2>&3

    Así conservás la interacción normal con la terminal mientras guardás el trace para análisis posterior.

  5. Aprovechar declare -p para inspeccionar variables
    Dentro de un trap o en puntos estratégicos, usá declare -p nombre_var para obtener una representación que pueda ser reutilizada como asignación.

  6. Preferir bashdb para sesiones interactivas
    Cuando necesitás establecer puntos de interrupción, observar la pila o ejecutar paso a paso, bashdb supera ampliamente a los echo y set -x.

Errores comunes

  • Olvidar comillar las expansiones de variables
    Esto provoca división de palabras y expansión de nombres de ruta no deseada, generando errores difíciles de rastrear. Siempre usá "$var" salvo que tengas un motivo específico para no hacerlo.

  • Confundir set -x con set -v
    Recordá que -x muestra lo que se ejecuta tras la expansión; -v muestra lo que se lee. Usar solo uno puede dejarte sin información crítica.

  • Depurar en producción sin controlar la verbosidad
    Un set -x dejado activado puede llenar los logs de información sensible o degradar el rendimiento. Usá variables de entorno o argumentos para activar el modo depuración solo cuando sea necesario.

  • No resetear PS4 después de modificarlo
    Si cambiás PS4 dentro de una función o subshell y no lo restaurá, afectará a los traces posteriores de forma inesperada.

  • Depender únicamente de echo para seguir el flujo
    Los echo pueden ser eliminados o comentados por error, y no muestran el contexto de expansión. Combinarlos con set -x o un trap es mucho más robusto.


Conclusión

Depurar scripts de Bash no tiene por qué ser una tarea de adivinanzas. Con las herramientas nativas como set -x, set -v y la señal DEBUG, podés obtener una visión detallada de cada paso que da el intérprete, desde la lectura de la línea hasta su ejecución y expansión. Cuando necesitás un control más profundo — puntos de interrupción, inspección de variables y seguimiento de la pila — , bashdb brinda una experiencia de depuración cercana a la de los depuradores tradicionales de otros lenguajes.

En este vimos:

  • Cómo activar y personalizar el rastreo con set -x y cómo ver el código tal como está con set -v.
  • Cómo construir un depurador ligero usando trap DEBUG para mostrar línea y comando antes de cada ejecución.
  • Una introducción práctica a bashdb, su instalación y los comandos más útiles para controlar la ejecución paso a paso.
  • Ejemplos concretos de salida de depuración al ejecutar un script que contiene errores intencionales, mostrando qué información brinda cada técnica.
  • Buenas prácticas para hacer que la depuración sea segura, efectiva y poco invasiva, así como los errores más frecuentes que debés evitar.

Aplicando estas técnicas en tus propios scripts, reducirás significativamente el tiempo dedicado a buscar fallos y aumentarás la confianza en la corrección de tus automatizaciones. ¡A depurar!


Nota: Si te interesado que implicar sobre scripts, podés explorar herramientas como shellcheck para análisis estático o integrar la depuración en tus pipelines de CI/CD para que los errores se detecten antes de llegar a producción.

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario