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é.