Una de las cabeceras más repetidas en scripts Bash modernos es:

set -euo pipefail

Suele describirse como el “modo estricto” de Bash. El nombre es útil, pero técnicamente no existe un único strict mode: esa línea simplemente activa tres comportamientos distintos que, juntos, hacen que muchos errores dejen de pasar silenciosamente.

La idea general es sencilla:

-e          aborta ante ciertos comandos fallidos
-u          falla al usar variables no definidas
-o pipefail hace visible el fallo de cualquier etapa de un pipeline

Eso mejora mucho scripts de CI, despliegues y automatización. Pero set -e tiene reglas menos intuitivas de lo que parece, y Bash ofrece muchas otras opciones que conviene conocer.

Primero: qué hace exactamente set

set es un builtin del shell. Entre otras cosas, permite activar y desactivar opciones de comportamiento del propio Bash.

Por ejemplo:

set -e

activa errexit, mientras que:

set +e

lo desactiva.

La convención puede parecer al revés de otras herramientas Unix: con set, el signo - normalmente activa y + desactiva una opción.

También existen nombres largos:

set -o errexit
set -o nounset
set -o pipefail
set -o xtrace

Y puedes inspeccionar el estado actual con:

set -o

-e: errexit

La intención de:

set -e

es terminar el script cuando un comando devuelve un código distinto de cero.

Ejemplo:

#!/usr/bin/env bash
set -e

cp archivo-inexistente.txt /tmp/
echo "no debería llegar aquí"

cp falla y Bash normalmente termina antes del echo.

Esto evita un patrón peligroso:

comando crítico falla
        ↓
el script sigue
        ↓
otros pasos trabajan con estado incompleto
        ↓
el error original queda oculto

Pero -e no significa literalmente “cualquier código != 0 aborta siempre”.

La gran trampa de -e: Bash necesita permitir fallos que forman parte de la lógica

Los shells usan códigos de salida también para tomar decisiones.

Por ejemplo:

if grep -q foo archivo.txt; then
  echo "encontrado"
fi

grep -q devuelve 1 cuando no encuentra el patrón. Eso no debería matar el script: es precisamente la condición que el if está evaluando.

Por eso Bash ignora errexit en varios contextos donde un fallo puede formar parte del control de flujo, entre ellos muchos usos dentro de:

if
while
until
&&
||
!
pipelines, excepto según sus reglas de estado final

Por ejemplo:

set -e

false || echo "false era esperado"
echo "el script continúa"

Esto es correcto y deliberado.

La consecuencia es importante: set -e es una red de seguridad, no un sistema formal de manejo de errores.

Para operaciones críticas sigue siendo buena práctica expresar claramente qué fallo esperamos y cuál no:

if ! deploy; then
  echo "deploy falló" >&2
  exit 1
fi

-u: nounset

Con:

set -u

usar una variable no definida se convierte en error.

Sin nounset:

echo "$API_TOKEN"

puede expandirse silenciosamente a una cadena vacía si API_TOKEN nunca fue definida.

En scripts de despliegue eso puede ser peligroso.

Con set -u, Bash detecta el problema inmediatamente.

Cuando una variable es opcional, se puede declarar la intención explícitamente:

name="${NAME:-default}"

O comprobar que sea obligatoria:

: "${API_TOKEN:?API_TOKEN es obligatoria}"

Ese segundo patrón produce un error claro si la variable falta o está vacía.

pipefail: el error escondido dentro de un pipeline

Considera:

generar_datos | transformar | guardar

Por defecto, el estado del pipeline suele ser el estado del último comando.

Así, algo conceptualmente parecido a:

false | true

puede terminar con código 0, porque true fue la última etapa.

Con:

set -o pipefail

el pipeline devuelve un estado de fallo si alguna etapa relevante falla.

Por eso:

set -euo pipefail

es mucho más útil que set -e solo para scripts que procesan datos mediante pipes.

Ejemplo típico:

curl -fsS "$URL" | jq '.items' > items.json

Sin pipefail, un fallo temprano puede quedar enmascarado por otro proceso que termina correctamente.

Entonces, ¿qué significa realmente set -euo pipefail?

Una forma más precisa de recordarlo es:

-e
haz visibles muchos fallos de comandos y no sigas alegremente

-u
no conviertas variables inexistentes en cadenas vacías sin avisar

pipefail
no escondas el fallo de una etapa intermedia de un pipeline

Es una excelente configuración base para muchos scripts no interactivos.

-E: heredar el trap ERR

Una extensión muy útil es:

set -E

También llamada errtrace.

Si defines:

trap 'echo "falló en línea $LINENO" >&2' ERR

-E hace que el trap ERR se herede en más contextos, como funciones y ciertos subshells.

Por eso se ve frecuentemente:

set -Eeuo pipefail

Ejemplo:

#!/usr/bin/env bash
set -Eeuo pipefail

trap 'echo "error en línea $LINENO" >&2' ERR

build() {
  false
}

build

Para scripts de CI o deploy, -E puede mejorar mucho el diagnóstico.

Ojo con subshells y command substitution

Aquí aparece otra sutileza.

Una sustitución como:

value="$(comando)"

se ejecuta en un entorno de subshell. Históricamente, Bash no siempre hereda errexit dentro de esos contextos de la manera que muchos esperan.

Bash dispone de:

shopt -s inherit_errexit

para hacer que las sustituciones de comandos hereden -e de forma más consistente. El modo POSIX también modifica este comportamiento.

Esto ilustra una regla general: si la corrección de tu script depende de una semántica muy específica de errexit, conviene probar explícitamente ese caso.

-x: xtrace, el debugger más simple de Bash

Para ver los comandos que Bash ejecuta:

set -x

Ejemplo:

set -x
name="demo"
echo "$name"
set +x

Bash imprimirá las expansiones y comandos ejecutados.

Es extremadamente útil en GitHub Actions, scripts de instalación y diagnósticos remotos.

Pero tiene un riesgo importante: puede imprimir secretos.

Por eso conviene apagarlo alrededor de credenciales:

set +x
TOKEN="$(obtener_token)"
set -x

O, mejor todavía, diseñar el script para no pasar secretos como argumentos visibles.

-v: imprime el script mientras se lee

set -v y set -x parecen similares, pero muestran cosas distintas.

set -v

muestra líneas de entrada según Bash las va leyendo.

set -x

muestra comandos después de expansiones relevantes, justo antes de ejecutarlos.

Para depuración práctica, -x suele ser el más útil.

-n: comprobar sin ejecutar

Con:

set -n

Bash lee comandos pero no los ejecuta.

Para validar un archivo completo normalmente es más claro usar:

bash -n deploy.sh

Es una comprobación barata que vale la pena ejecutar en CI para scripts shell.

No sustituye ShellCheck ni tests, pero detecta errores de sintaxis antes de tocar producción.

-C: noclobber, evita sobrescribir por accidente

Con:

set -C

una redirección como:

echo datos > archivo.txt

falla si el archivo ya existe.

Esto protege contra sobrescrituras accidentales.

Si realmente deseas forzarla, Bash permite:

echo datos >| archivo.txt

Puede ser útil en scripts que generan artefactos donde sobrescribir silenciosamente sería un error.

-a: exportar variables automáticamente

Con:

set -a

las variables creadas o modificadas pasan automáticamente al entorno de procesos hijos.

Ejemplo típico:

set -a
source .env
set +a

python app.py

Esto hace que las variables leídas desde .env queden exportadas sin escribir export una por una.

Pero hay que usarlo con cuidado: también puedes exportar variables que nunca pretendiste propagar.

-f: desactivar globbing

Normalmente Bash expande patrones como:

*.log

antes de ejecutar el comando.

Con:

set -f

se desactiva ese pathname expansion.

Puede ser útil cuando trabajas con entrada que contiene *, ? o [] y no quieres que el shell los interprete como nombres de archivo.

Se reactiva con:

set +f

Otras opciones de set

Bash ofrece bastantes más. Algunas son más relevantes en shells interactivos que en scripts:

-b  notify      informa inmediatamente terminaciones de jobs en background
-m  monitor     activa job control
-h  hashall     recuerda rutas de comandos encontrados
-B  braceexpand activa expansión {a,b}
-H  histexpand  activa expansión de historial con !
-P  physical    evita seguir symlinks lógicos al resolver ciertos directorios
-k  keyword     exporta más assignments puestos en argumentos de comandos
-p  privileged  cambia comportamiento cuando hay diferencias de UID/GID
-t  onecmd      ejecuta un comando y sale

También existen modos interactivos mediante set -o, como:

set -o vi
set -o emacs

que cambian el modo de edición de línea.

set -o es tu tabla de verdad

En vez de memorizar cada opción, Bash puede enseñarte su estado:

set -o

Obtendrás algo parecido a:

errexit         off
nounset         off
pipefail        off
xtrace          off
noclobber       off
...

Y para una forma reutilizable que muestra opciones activadas/desactivadas con comandos set:

set +o

Esto es útil cuando quieres capturar y restaurar configuración del shell.

Una cabecera práctica para CI y deploy

Para muchos scripts de automatización, una base razonable es:

#!/usr/bin/env bash
set -Eeuo pipefail

Y durante troubleshooting:

set -x

solo en la sección que necesitas observar.

También ayuda instalar un trap con contexto:

trap 'printf "ERROR: %s:%s: %s\n" "$0" "$LINENO" "$BASH_COMMAND" >&2' ERR

No convierte Bash en un lenguaje con excepciones estructuradas, pero deja un rastro mucho más útil cuando algo falla en CI.

No conviertas el “strict mode” en dogma

Hay scripts donde set -e resulta incómodo porque muchos comandos usan estados distintos de cero como parte normal de su API.

También hay código heredado que depende de variables opcionales y rompe inmediatamente con set -u.

La decisión correcta no es añadir set -euo pipefail mecánicamente a todo archivo. Es entender qué garantías aporta cada opción y escribir explícitamente los lugares donde el fallo es esperado.

Por ejemplo:

if grep -q pattern file; then
  echo "sí"
else
  echo "no"
fi

es mejor que intentar obligar a errexit a adivinar nuestra intención.

La idea que conviene recordar

set -euo pipefail no hace que Bash sea mágicamente seguro.

Lo que hace es cambiar varios defaults peligrosamente permisivos:

fallo silencioso          → fallo visible
variable inexistente      → error
pipeline parcialmente roto → error

Y las demás opciones completan la caja de herramientas:

-E  mejores traps ERR
-x  trazado de ejecución
-v  trazado de entrada
-n  validación sin ejecución
-C  evitar overwrite accidental
-a  export automático
-f  desactivar globbing

El salto importante ocurre cuando dejamos de copiar set -euo pipefail como una fórmula y empezamos a entender qué semántica del shell estamos cambiando y por qué.