1. Dos respuestas por comando
Todo comando que se ejecuta da dos respuestas. La primera es lo que escribe en su salida estándar y en la de errores: texto, pensado sobre todo para las personas. La segunda es un número entero que entrega al terminar, y que no aparece en pantalla.
El código de salida (exit status) es un número entre 0 y 255 que todo proceso devuelve al terminar, para informar a quien lo ejecutó si cumplió su tarea. Por convención, 0 significa éxito y cualquier otro valor, algún tipo de fallo.
Un script no debería decidir qué hacer leyendo mensajes. Los mensajes cambian de una versión a otra, dependen del idioma configurado y no tienen un formato fijo. El código de salida, en cambio, es una interfaz estable y pensada para programas. El tema 25 del curso de Terminal lo presentó junto con && y ||; aquí lo estudiamos a fondo, porque desde ahora todos los scripts dependerán de él.
2. Leer el código: $?
La variable especial $? contiene el código de salida del último comando que terminó:
$ ls /etc/hostname
/etc/hostname
$ echo $?
0
$ ls /no/existe
ls: cannot access '/no/existe': No such file or directory
$ echo $?
2
La palabra clave es último. Cada comando que se ejecuta reemplaza el valor de $?, incluidos echo, [ ] y cualquier otro. Por eso hay que leerlo inmediatamente, o guardarlo en una variable antes de hacer otra cosa:
$ ls /no/existe 2> /dev/null
$ echo "Falló"; echo "Código: $?" # $? es ahora el del primer echo
Falló
Código: 0
$ ls /no/existe 2> /dev/null
$ codigo=$? # guardarlo enseguida
$ echo "Falló"; echo "Código: $codigo"
Falló
Código: 2
En la práctica, como veremos en la sección 8, casi nunca hace falta leer $?: lo habitual es usar el comando directamente como condición. $? se reserva para cuando importa el valor exacto del código, y no solo si fue cero.
3. El cero es éxito
A primera vista, la convención parece al revés: en casi todos los lenguajes de programación, y en la aritmética de Bash, el 0 es «falso». La razón es práctica: hay una sola forma de que algo salga bien, y muchas de que salga mal. El cero indica éxito, y los demás 255 valores quedan libres para distinguir por qué falló.
Dos comandos existen solo para producir un código: true, que termina siempre con 0, y false, que termina siempre con 1. Parecen inútiles, pero sirven para bucles infinitos (while true), para probar cómo se comporta un script ante un fallo, y como valores de configuración que se ejecutan: simular=true y luego if $simular; then.
4. Qué devuelven los comandos
Cada programa decide el significado de sus códigos distintos de cero, y lo documenta en su página de manual, normalmente en una sección llamada EXIT STATUS. Muchos se limitan a 0 y 1, pero varios de los más usados distinguen casos, y conocerlos evita errores:
| Comando | 0 | 1 | Otros |
|---|---|---|---|
grep | Encontró al menos una coincidencia. | No encontró nada. No es un error. | 2: error real, como un archivo inexistente. |
diff, cmp | Los archivos son iguales. | Son distintos. Tampoco es un error. | 2: no se pudieron comparar. |
test, [ ], [[ ]] | La condición es cierta. | Es falsa. | 2: expresión mal escrita. |
ls | Todo bien. | Problemas menores. | 2: problemas graves, como un argumento inexistente. |
curl | Transferencia completa. | Protocolo no admitido. | 6: no resolvió el nombre; 7: no conectó; 22: error HTTP, con --fail; 28: tiempo agotado. |
rsync | Todo bien. | Error de sintaxis. | 23: transferencia parcial; 24: archivos que desaparecieron durante la copia. |
ping | Hubo respuesta. | No hubo respuesta. | 2: otro error. |
Los casos de grep y diff son los más importantes, porque rompen la equivalencia entre «distinto de cero» y «error». Para grep, no encontrar nada es una respuesta, no una falla: el código 1 dice «no», y solo el 2 dice «no pude hacer mi trabajo».
$ grep -q ERROR app.log; echo $?
0
$ grep -q FATAL app.log; echo $? # no aparece: respuesta negativa
1
$ grep -q FATAL noexiste.log; echo $? # no pudo leer el archivo: error
grep: noexiste.log: No such file or directory
2
La distinción tendrá consecuencias en el tema 23: un script que se detiene ante cualquier código distinto de cero se detendría también cuando grep simplemente no encuentra lo que busca.
5. Los códigos que pone la shell
Algunos códigos no los elige el comando, sino Bash, cuando el comando ni siquiera pudo ejecutarse o fue interrumpido. Reconocerlos permite diagnosticar un fallo sin ver ningún mensaje, por ejemplo en el registro de una tarea de cron:
| Código | Significado | Causa típica |
|---|---|---|
126 | El comando existe, pero no se pudo ejecutar. | Falta el permiso de ejecución, o es un directorio. |
127 | El comando no existe. | Un nombre mal escrito, o un PATH incompleto, como el de cron. |
128 + n | El proceso terminó por la señal número n. | Lo interrumpieron o lo mataron. |
130 | Señal 2, INT. | Se pulsó Ctrl+C. |
137 | Señal 9, KILL. | kill -9, o el sistema lo mató por falta de memoria. |
141 | Señal 13, PIPE. | Escribía en una tubería cuyo lector ya había terminado (sección 10). |
143 | Señal 15, TERM. | kill sin opciones, o el apagado de un servicio. |
$ comandoinexistente; echo $?
bash: comandoinexistente: command not found
127
$ ./sin-permiso.sh; echo $?
bash: ./sin-permiso.sh: Permission denied
126
$ sleep 300 & kill -9 $!; wait $!; echo $?
137
$ kill -l 137 # ¿qué señal es?
KILL
kill -l traduce un número de señal, o un código de salida mayor que 128, a su nombre. Un 137 en el registro de un proceso que nadie mató a mano suele significar que el sistema se quedó sin memoria y eligió terminarlo.
6. exit
Un script termina con el código de su último comando, como vimos en el tema 2, salvo que use exit, que lo termina en ese punto con el código indicado:
| Forma | Efecto |
|---|---|
exit 0 | Termina indicando éxito. |
exit 1, exit 2… | Termina indicando un fallo, con ese código. |
exit | Termina con el código del último comando ejecutado. |
El código se guarda en un byte, de modo que se toma módulo 256. Un valor fuera del rango no da error, sino un código inesperado:
$ bash -c 'exit 256'; echo $? # 256 mod 256
0
$ bash -c 'exit 300'; echo $?
44
$ bash -c 'exit -1'; echo $?
255
El primer caso es peligroso: un script que devolviera la cantidad de errores con exit "$errores" informaría éxito si hubo exactamente 256. Si se quiere comunicar una cantidad, debe ir en la salida, no en el código. Dentro de una función, exit termina el script entero; para terminar solo la función con un código se usa return, como veremos en el tema 19.
7. Diseñar los códigos de un script
Si un script lo van a usar otros scripts, sus códigos de salida son parte de su interfaz, tanto como sus argumentos. Unas pautas simples:
- 0 solo si se hizo todo lo pedido. Un respaldo que copió nueve de diez directorios no tuvo éxito.
- 1 para el fallo general, cuando no hace falta distinguir causas.
- 2 para un uso incorrecto: argumentos que faltan o no son válidos. Es la convención de las herramientas de GNU y de los comandos internos de Bash.
- Otros valores para fallos que quien llama necesite distinguir, porque reaccionará distinto ante cada uno. Valores bajos, entre 3 y 125, para no confundirse con los reservados.
- Documentarlos en la cabecera, como propusimos en el tema 4.
# Salida: 0 el respaldo se completó y se verificó
# 1 error general
# 2 uso incorrecto
# 3 el destino no tiene espacio suficiente
# 4 el respaldo se creó, pero la verificación falló
readonly E_USO=2 E_ESPACIO=3 E_VERIFICACION=4
Las constantes con nombre hacen que el código se lea solo: exit "$E_ESPACIO" se entiende mejor que exit 3. Y con códigos distintos, un script que llama a respaldo puede, por ejemplo, borrar respaldos viejos y reintentar ante un código 3, y dar una alarma ante un 4.
8. Usar el código en condiciones
Toda la lógica de Bash se apoya en los códigos de salida. if, while, && y || no evalúan valores verdaderos o falsos, como en otros lenguajes: ejecutan un comando y miran su código. Cero es «sí»; cualquier otro, «no».
if grep -q ERROR app.log; then
echo "Hay errores en el registro"
fi
if ! ping -c1 -W2 servidor > /dev/null; then # ! invierte el resultado
echo "El servidor no responde" >&2
fi
mkdir -p "$destino" || exit 1 # si falla, terminar
cd "$destino" && rm -f -- *.tmp # borrar solo si se pudo entrar
La opción -q de grep lo deja en silencio: no muestra las líneas, solo devuelve el código. Es la forma de usar grep como pregunta. Muchos comandos tienen una opción similar, o se los silencia redirigiendo su salida a /dev/null, como el ping del ejemplo.
Un patrón muy frecuente es «hacer esto, o terminar con un mensaje». Las llaves agrupan varios comandos para que actúen como uno solo después de ||:
cp -- "$origen" "$destino" || { echo "No se pudo copiar $origen" >&2; exit 1; }
Las llaves necesitan un espacio después de { y un ; antes de }. A diferencia de los paréntesis, no crean un proceso aparte, y por eso el exit termina el script y no una subshell.
$? si puedes preguntar por el comando
La forma comando; if [ $? -eq 0 ]; then funciona, pero es indirecta y frágil: si alguien agrega una línea entre el comando y el if, por ejemplo un mensaje de registro, la condición pasa a evaluar ese otro comando. if comando; then dice lo mismo, de forma directa. ShellCheck sugiere el cambio con el aviso SC2181.
9. Capturar el código sin perderlo
Cuando además de saber si el comando falló hace falta el código exacto, hay que capturarlo sin que otro comando lo reemplace. Dentro de la rama else de un if, $? conserva el código del comando de la condición, lo que permite reaccionar distinto según el fallo:
if rsync -a "$origen/" "$destino/"; then
echo "Copia completa"
else
codigo=$?
if (( codigo == 24 )); then
echo "Aviso: algunos archivos desaparecieron durante la copia" >&2
else
echo "Error de rsync: código $codigo" >&2
exit 1
fi
fi
Con la sustitución de comandos también se conserva el código: como vimos en el tema 5, salida=$(comando) toma el código de comando. Pero hay una trampa: si la asignación está precedida de local, export, readonly o declare, el código es el de ese comando, que siempre tiene éxito.
$ salida=$(false); echo $?
1
$ export salida=$(false); echo $? # el código es el de export
0
Por eso, dentro de una función, se declara la variable local en una línea y se asigna en la siguiente: local salida y luego salida=$(comando). Es el aviso SC2155 de ShellCheck que vimos en el tema 4, y lo retomaremos en el tema 18.
10. Tuberías: pipefail y PIPESTATUS
Una tubería ejecuta varios comandos, pero devuelve un solo código: el del último. Los fallos de los anteriores se pierden:
$ cat /no/existe | sort | head -5; echo $?
cat: /no/existe: No such file or directory
0
El archivo no existía, pero la tubería informa éxito, porque head terminó bien. La opción pipefail cambia la regla: el código de la tubería pasa a ser el del último comando que falló, o 0 si ninguno falló. Es la tercera parte de set -euo pipefail.
$ set -o pipefail
$ cat /no/existe | sort | head -5; echo $?
cat: /no/existe: No such file or directory
1
Para conocer el código de cada parte por separado, Bash guarda todos en el array PIPESTATUS, que, como $?, se reemplaza con el comando siguiente:
$ true | false | true
$ echo "${PIPESTATUS[@]}"
0 1 0
Con pipefail aparece un fallo inesperado. En comando | head -1, head termina en cuanto lee la primera línea, y si comando sigue escribiendo, el sistema lo interrumpe con la señal PIPE: código 141. Sin pipefail nadie lo nota; con él, la tubería entera «falla». Por ejemplo, yes | head -1 devuelve 141. Ocurre con productores que escriben mucho, como yes, find o un cat de un archivo grande, seguidos de head o de grep -q, que también deja de leer en cuanto encuentra lo que busca. Lo retomaremos en el tema 23.
11. El código de las demás construcciones
No solo los comandos simples tienen código de salida. Cada construcción de Bash devuelve uno, según reglas que conviene conocer para no llevarse sorpresas:
| Construcción | Su código de salida |
|---|---|
a && b, a || b | El del último comando que se ejecutó. |
! comando | 0 si el comando falló; 1 si tuvo éxito. |
if … fi | El del último comando ejecutado dentro de la rama elegida; 0 si no se ejecutó ninguna rama. |
for, while | El del último comando ejecutado en el cuerpo; 0 si no dio ninguna vuelta. |
{ …; }, ( … ) | El del último comando del grupo. |
var=$(comando) | El de comando. |
local var=$(comando) | El de local: casi siempre 0. |
| Una función | El de su último comando, o el que indique return (tema 19). |
| Un script | El de su último comando, o el que indique exit. |
La regla de las funciones y los scripts explica un error sutil: un script cuya última línea es [[ -n "$x" ]] && echo "$x" termina con código 1 cuando x está vacía, aunque no haya fallado nada. Si a ese script lo llama otro, o cron, parecerá un error. Cuando el final de un script puede dejar un código engañoso, conviene terminarlo con un exit 0 explícito.
12. Un script completo
Este script comprueba que una lista de sitios web responda. Usa los códigos de salida en las dos direcciones: lee el de curl para explicar cada fallo con palabras, y devuelve uno propio, documentado, para que pueda usarse desde cron o desde otro script.
#!/usr/bin/env bash
#
# verificar-sitios — comprueba que una lista de sitios web responda.
#
# Uso: verificar-sitios URL...
# Salida: 0 si todos responden; 1 si alguno falla; 2 si el uso es incorrecto.
#
set -euo pipefail
if (( $# == 0 )); then
echo "Uso: ${0##*/} URL..." >&2
exit 2
fi
# Los códigos de salida de curl más frecuentes (ver «man curl», EXIT CODES).
declare -A motivo=(
[6]="no se pudo resolver el nombre"
[7]="no se pudo conectar"
[22]="el servidor respondió con un error HTTP"
[28]="se agotó el tiempo de espera"
[60]="el certificado TLS no es válido"
)
fallidos=0
for url in "$@"; do
if curl --fail --silent --output /dev/null --max-time 10 "$url"; then
printf 'OK %s\n' "$url"
else
codigo=$? # el código de curl, conservado en el else
printf 'FALLA %s (curl %d: %s)\n' "$url" "$codigo" "${motivo[$codigo]:-ver «man curl»}"
fallidos=$(( fallidos + 1 ))
fi
done
if (( fallidos > 0 )); then
printf '%d de %d sitios fallaron\n' "$fallidos" "$#" >&2
exit 1
fi
$ verificar-sitios https://example.com https://example.com/no-existe http://nohay.invalid http://127.0.0.1:9
OK https://example.com
FALLA https://example.com/no-existe (curl 22: el servidor respondió con un error HTTP)
FALLA http://nohay.invalid (curl 6: no se pudo resolver el nombre)
FALLA http://127.0.0.1:9 (curl 7: no se pudo conectar)
3 de 4 sitios fallaron
$ echo $?
1
$ verificar-sitios; echo $?
Uso: verificar-sitios URL...
2
- El
ifalrededor decurlhace dos cosas: decide entre las dos ramas y, como el fallo ocurre dentro de una condición, evita queset -edetenga el script al primer sitio caído. Dentro delelse,$?todavía es el código decurl. --failhace quecurltrate las respuestas HTTP de error, como 404 o 500, como un fallo con código 22. Sin esa opción, una página de error sería un «éxito», porque la transferencia en sí funcionó.- El array asociativo del tema 8 traduce los códigos a palabras, y
${motivo[$codigo]:-…}cubre los que no están en la tabla. - El resumen va a la salida de errores, y la salida estándar queda solo con el informe por sitio. Programado en
cron, el código 1 permite enviar un aviso solo cuando algo falló, como veremos en el tema 40.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
cmd; echo "fin"; echo $? | Muestra el código de echo. | Guardar codigo=$? justo después de cmd. |
cmd; if [ $? -eq 0 ] | Funciona, pero se rompe si se intercala una línea. | if cmd; then |
if [ $(grep x f) ] | Evalúa el texto encontrado, no el código de grep. | if grep -q x f; then |
Tratar el 1 de grep como error | Un «no encontrado» detiene o alarma. | Distinguir 1 (no) de 2 (error). |
exit "$errores" | Con 256 errores, informa éxito. | exit 1, y la cantidad en la salida. |
local x=$(cmd) | El código de cmd se pierde. | local x y luego x=$(cmd). |
a | b sin pipefail | Un fallo de a pasa inadvertido. | set -o pipefail, o PIPESTATUS. |
cmd || ( echo error; exit 1 ) | El exit termina la subshell, no el script. | Llaves: { echo error; exit 1; } |
Última línea [[ … ]] && … | El script termina con 1 sin haber fallado. | exit 0 explícito, o un if. |
14. Resumen
- Todo comando devuelve un código de salida entre 0 y 255. Es la interfaz pensada para que los programas, y los scripts, sepan cómo le fue.
$?contiene el código del último comando, y cada comando lo reemplaza: se lee enseguida o se guarda en una variable.- 0 es éxito; cualquier otro valor, un fallo.
trueyfalsesolo producen 0 y 1. - Cada comando define sus códigos. Para
grepydiff, 1 es una respuesta negativa y 2 un error. - La shell reserva 126 (no se pudo ejecutar), 127 (no existe) y 128 + n (terminado por la señal n): 130 es Ctrl+C, 137
kill -9, 143kill. exit ntermina el script con el códigon, tomado módulo 256.- Un buen script devuelve 0 solo si hizo todo, 1 para el fallo general, 2 para el uso incorrecto, y documenta sus códigos.
if,while,&&y||ejecutan un comando y miran su código. Se escribeif comando, noif [ $? -eq 0 ].- En la rama
else,$?es todavía el código de la condición. local,exportyreadonlydelante de una asignación con$( )ocultan el código del comando.- Una tubería devuelve el código del último comando.
pipefailhace que cuente el último que falló, yPIPESTATUSguarda todos. - Con
pipefail, un productor cortado porheadda 141.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué un script debería decidir según el código de salida y no según los mensajes de un comando?
Porque los mensajes son para personas: cambian entre versiones, dependen del idioma y no tienen un formato fijo. El código de salida es una interfaz estable, pensada para que la lean los programas.
2. ¿Qué muestra ls /no/existe; echo "error"; echo $? en su última línea, y por qué?
0, porque $? contiene el código del último comando, que es el primer echo, y ese terminó bien. El código de ls se perdió.
3. ¿Por qué el éxito es 0 y no 1?
Porque hay una sola forma de tener éxito y muchas de fallar. Con el 0 para el éxito, quedan 255 valores para distinguir distintas causas de fallo.
4. ¿Qué significan los códigos 0, 1 y 2 de grep?
0: encontró al menos una coincidencia. 1: no encontró ninguna, que es una respuesta, no un error. 2: ocurrió un error, como un archivo que no se pudo leer.
5. Una tarea de cron registra el código 127. ¿Qué pasó, probablemente?
Que un comando no se encontró. En cron, la causa típica es que el PATH es más reducido que el de la terminal, y un programa instalado en un directorio no estándar no se halla.
6. ¿Qué significa un código 137, y cómo se averigua el nombre de la señal?
Que el proceso terminó por la señal 9 (137 − 128), KILL. Con kill -l 137 se obtiene el nombre. Si nadie lo mató a mano, suele indicar que el sistema se quedó sin memoria.
7. ¿Con qué código termina un script que ejecuta exit 258?
Con 2, porque el código se toma módulo 256.
8. ¿Qué código conviene usar cuando un script se llama con argumentos incorrectos?
2, la convención de las herramientas de GNU y de los comandos internos de Bash para un uso incorrecto.
9. ¿Por qué es preferible if grep -q x f; then a grep -q x f; if [ $? -eq 0 ]; then?
Porque es directo y no depende de que $? siga intacto. En la segunda forma, cualquier línea que se agregue entre el comando y el if cambiaría lo que se evalúa.
10. ¿Cómo se obtiene el código exacto de un comando que falló dentro de la condición de un if?
Leyendo $? al comienzo de la rama else, donde todavía contiene el código del comando de la condición.
11. ¿Qué devuelve local x=$(false), y por qué es un problema?
Devuelve 0, el código de local, que tuvo éxito. El fallo de false queda oculto. Se evita declarando y asignando en dos líneas.
12. Sin pipefail, ¿qué código devuelve cat /no/existe | wc -l? ¿Y con pipefail?
Sin pipefail, 0, el código de wc. Con pipefail, 1, el de cat, que fue el último en fallar.
13. ¿Qué contiene PIPESTATUS?
Un array con el código de salida de cada comando de la última tubería, en orden.
14. ¿Por qué yes | head -1 devuelve 141 con pipefail?
Porque head termina tras leer una línea, y cuando yes intenta seguir escribiendo en la tubería sin lector, recibe la señal PIPE (13) y termina con 128 + 13. Con pipefail, ese código pasa a ser el de la tubería.
15. ¿Por qué cmd || ( echo "error"; exit 1 ) no termina el script?
Porque los paréntesis crean una subshell, un proceso aparte, y el exit termina esa subshell. Para terminar el script se usan llaves: { echo "error"; exit 1; }.
Para practicar
- Ejecuta
grepsobre un archivo con un texto que aparece, con uno que no aparece y sobre un archivo inexistente, y anota los tres códigos. - Haz lo mismo con
diff: dos archivos iguales, dos distintos y uno inexistente. - Provoca los códigos 126 y 127, y el 130 interrumpiendo un
sleep 60con Ctrl+C. Usakill -lpara traducir el último. - Escribe un script que termine con
exit 256y otro conexit 257. Comprueba sus códigos. - Compara el código de
cat /no/existe | sortcon y sinset -o pipefail, y muestraPIPESTATUSen los dos casos. - Escribe una función que contenga
local x=$(false)y comprueba su código. Corrígela. - Revisa los scripts que escribiste en los temas anteriores: ¿devuelven un código distinto de cero cuando fallan? ¿Hay alguno cuya última línea pueda dejar un 1 engañoso?
- Agrega a
verificar-sitiosuna opción, por variable de entorno, que cambie el tiempo límite decurl, y prueba el código 28 con un tiempo muy corto.