Saltar a contenido

2026

Bash debug mode

Depurar un script de bash suele consistir en insertar un montón de echo que luego hay que borrar.

Un método mejor es usar set -x, que activa lo que podríamos llamar bash debug mode.

de la documentación bash

-x. Print a trace of simple commands, for commands, case commands, select commands, and arithmetic for commands and their arguments or associated word lists to the standard error after they are expanded and before they are executed. The shell prints the expanded value of the PS4 variable before the command and its expanded arguments.

Esta opción hace un print de cada comando del script a stderr antes de ejecutarlo.

Los parámetros (parameters) se expanden antes del print por lo que veremos los valores reales (arguments).

Podemos simplemente añadirlo al script cuando estemos depurando, y eliminarlo después. O, incluir la lógica en el propio script mediante parámetros y variables de entorno. Lo bueno de la variable de entorno es que podríamos tener varios scripts que la compartan de modo que activemos el modo debug para todos los scripts a la vez

#!/usr/bin/env bash

DEBUG="${GLOBAL_DEBUG_MODE:-false}"

while [[ $# -gt 0 ]]; do
    case $1 in
        --debug) DEBUG=true ;;
    esac
    shift
done

"${DEBUG}" && set -x

a=5
echo "${a}"
echo "bar"

Salida:

$ GLOBAL_DEBUG_MODE=true ./my-script.sh

+ a=5
+ echo 5
5
+ echo bar
bar

Dos trucos adicionales en los que fijarse que nos permiten un poco de magia extra

# Use '2$>' instead of '2>' to combine stderr and stdout
$ PS4='\D{%F:%T} >> ' ./script2.sh --debug 2> debug.log

5
bar

$ cat debug.log

2026-02-08:19:30:51 >> a=5
2026-02-08:19:30:51 >> echo 5
2026-02-08:19:30:51 >> echo bar

Bash: Variables indefinidas (unset) vs vacías (empty)

Bash es un lenguaje particular respecto a las variables que no han sido definidas previamente.

En Python, por ejemplo, las reglas son claras: una variable debe estar declarada antes de usarse. Si esa variable no existe, el intérprete lanzará un error.

>>> print (my_var)
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    print (my_var)
           ^^^^^^
NameError: name 'my_var' is not defined

>>> my_var="foo"
>>> print (my_var)
foo

En bash, por defecto, no se distingue entre una variable vacía y una indefinida. Al acceder a una variable no definida, bash no lanza errores y no detiene el script. Simplemente asume la asume cómo un empty string.

Esto, que parece cómodo para pequeños scripts, es también causa de muchos desastres: rm -rf "${build_dir}/".

Empty vs Null

En este artículo usaremos exclusivamente el termino empty variable o variable vacía.

Pero para bash no hay diferencia práctica entre null y empty. A veces también se habla de ese valor cómo empty string o null string

En la práctica el valor de estas dos variables es el mismo:

explicit_empty_or_null_string=""
implicit_empty_or_null_string=

Empty vs Unset

Pero, aunque bash intente disimularlo, si hay diferencias entre empty o null y unset:

  • empty: La variable existe, tiene un espacio asignado en memoria, pero su contenido es una cadena de longitud cero.
  • unset: La variable no existe en el entorno actual. No tiene un espacio de memoria asignado.

Sin "protecciones", ambos tipos se comportan igual al expandirse:

#!/usr/bin/env bash
# Sin 'set -u'

unset explicit_not_existent_var # variable indefinida
explicit_empty_string=""  # variable vacía
implicit_empty_string=    # variable vacía

# En los cuatro casos se imprimirán líneas vacías
echo "A: ${implicit_no_existent_var}"
echo "B: ${explicit_not_existent_var}"
echo "C: ${explicit_empty_string}"
echo "D: ${implicit_empty_string}"

Unbound variables

La cosa cambia cuando activamos el modo de bash de protección ante unbound variables

unset vs unbound

No hay diferencias a nivel práctico entre el término unset y el término unbound en bash y se pueden usar cómo sinónimos. Lo que sucede es que al usar una variable no definida bajo set -u el mensaje de error (bash: foo: unbound variable) hace referencia a unbound,mientras que los comandos en sí hacen referencia a unset

Cuando usamos set -u (o set -o nounset) cambiamos "al modo protección". Una práctica que forma parte del llamado bash strict mode y que debería usarse en el 95% de los casos.

de la documentación bash

Treat unset variables and parameters other than the special parameters '@' or '', or array variables subscripted with '@' or '', as an error when performing parameter expansion. An error message will be written to the standard error, and a non-interactive shell will exit.

Siguiendo el ejemplo anterior:

#!/usr/bin/env bash

set -u

unset explicit_not_existent_var # variable indefinida
explicit_empty_string=""  # variable vacía
implicit_empty_string=    # variable vacía

echo "A: ${implicit_not_existent_var}" # bash: implicit_no_existent_var: unbound variable
echo "B: ${explicit_not_existent_var}" # bash: explicit_no_existent_var: unbound variable
echo "C: ${explicit_empty_string}" # imprime una cadena vacía
echo "D: ${implicit_empty_string}" # imprime una cadena vacía

Operaciones útiles

Hay varias "operaciones" que es útil conocer para validar estas variables.

La primera es:

  • noop :. Llamada a veces operación null, permite evaluar las variables si que se ejecute su resultado. Veremos su utilidad en ejemplos posteriores.

Conditional Expressions

Lo más básico es usar la expresiones condicionales del [[ compound command y los builtin commands test y [

De la documentación de expresiones condicionales:

  • -v varname. True if the shell variable varname is set (has been assigned a value). If varname is an indexed array variable name subscripted by @ or *, this returns true if the array has any set elements. If varname is an associative array variable name subscripted by @ or *, this returns true if an element with that key is set.
  • -z string. True if the length of string is zero.
  • -n string. True if the length of string is non-zero.

Usaremos estas expresiones dentro de un if o con operadores cómo && o ||. El que en general debemos usar es -v dado que -z y -n dan error en modo set -u con variables indefinidas.

Danger

-v espera un nombre de variable, no se debe poner el $. Si estamos trabajando con referencias hay que usar -R.

A modo de ejemplo

set -u
unset foo

if [[ -z "${foo}" ]]; then echo "'foo' is not set"; exit 1; fi # bash: foo: unbound variable
[[ -z "${foo}" ]] && echo "'foo' is not set" && exit 1 # bash: foo: unbound variable

if ! [[ -v foo ]]; then echo "'foo' is not set"; exit 1; fi # 'foo' is not set
[[ -z foo ]] || echo "'foo' is not set" && exit 1 # 'foo' is not set
[[ -z foo ]] || foo='DEFAULT_VALUE' # DEFAULT_VALUE is assigned to foo

Parameter Expansion

Para asignar valores por defecto o "fallar pronto" si una variable está vacía el parameter expansion de bash es más elegante que las expresiones condiciones

  • Valor por defecto (safe fallback) :-. Si la variable es unset o empty aplica un valor por defecto sin modificar la variable original.
  • Asignación por defecto :=. Si la variable es unset o empty asigna un valor por defecto a la variable original.
  • Fallo Temprano :?. Si la variable es unset o empty, e independientemente de haber usado set -u se aborta el script con un mensaje.
# Si 'nombre' no existe o está vacío, usa "Mundo".
echo "Hola ${nombre:-Mundo}"

# Asigna "rm" a FAVORITE_COMMAND si no estaba definido.
# Usamos noop (:) para que Bash evalúe la expresión sin ejecutar el resultado. Sin `:`, ejecutaría el `rm` o lo que contenga la variable FAVORITE_COMMAND
: ${FAVORITE_COMMAND:=rm}

# Si build_dir no está seteado, aborta imprimiendo el mensaje.
rm -rf "${build_dir:?Error: Directorio no definido}/"

Algunas referencias extras

Conclusiones: Guía de estilo

Los scripts en bash son potentes y flexibles pero es fácil que el código sea difícil de leer o con bugs ocasionales pero catastróficos.

Dentro de mis normas para bash en lo referente a variables vacías y no definidas están:

Usar set -u. Hay pocas situaciones en que este modo no sea el correcto. Y en partes concretas de un script se puede desactivar y volver a activar.

#!/usr/bin/env bash

set -u

echo "Start"

set +u
echo "Something weird related to unbound variables"
set -u
echo "Come back to safety"

Usar :- para asignar valores por defecto. No uso el comando de asignación :=. Por nada en especial, simplemente me permite reducir la cantidad de formas distinta de hacer lo mismo. No se pueden usar con $1.

#!/usr/bin/env bash

set -u

"${foo:=World}" # error, intentará ejecutar el comando `World`
: "${1:=World}" # error, no se puede asignar a $1
: "${foo:=World}" # No da error, pero "reducimos la API a conocer"

# Me gustan más estas soluciones
foo="${1:-World}"
foo="${foo:-World}"

die(){
  # Call like `die "File not found"` or `die`
  local error=${1:-Undefined error}
  echo "$0: $LINE $error" >&2
  exit 1
}

# Si no queremos que salte unbound pero no nos preocupa que sea empty, podemos dejar el
# valor por defecto vacío
info() {
    # Will print the message `info "this is a message"` or an empty line `info`
    local msg="${1:-}"
    echo "${msg}"
}

Usar :? para comprobar cuando una variable es vacía o indefinida.

Ejemplos:

#!/usr/bin/env bash

set -u

user="${1:?Mandatory parameter for 'user' is missing}"

build_dir_path=$(find . -type d -iname 'build_dir')
: ${A:?'build_dir' folder is not found}

: ${VIRTUAL_ENV:?"virtualenv should be activated before continue"}

# Prefiero la versión anterior a estas
[[ -v VIRTUAL_ENV ]] && echo "virtualenv should be activated before continue" && exit 1
# No usar -z porqué genera un unbound
if ! [[ -v VIRTUAL_ENV ]] ; then
    echo "virtualenv should be activated before continue"
    exit 1
fi

Para entender mejor otras opciones

# Estamos intentando usar una unbound variable, da el mensaje genérico al respecto.
: ${VIRTUAL_ENV} # bash: VIRTUAL_ENV: unbound variable

# Genera su propio mensaje de error, distinto al habitual
: ${VIRTUAL_ENV:?} # bash: VIRTUAL_ENV: parameter null or not set

# definimos la variable pero en blanco
VIRTUAL_ENV=

# Sigue detectando que está vacía
: ${VIRTUAL_ENV:?} # bash: VIRTUAL_ENV: parameter null or not set
: ${VIRTUAL_ENV} # No da error, simplemente es una cadena vacía que no se ejecuta