3.6.4. Captura de eventos

En ocasiones nos interesa ejecutar una acción o un conjunto de acciones al término del programa (p.e. borrar un archivo temporal creado durante la ejecución del script). Para ello existe la orden interna trap:

trap [cadena_de_ordenes] SEÑAL1 [SEÑAL2 ...]

La cadena_de_ordenes es una cadena que contiene la orden u órdenes que queremos ejecutar, mientras que a continuación se incluye la lista de señales para las que queremos que se ejecuten las órdenes. Por ejemplo:

TMPFILE=$(mktemp)
trap 'rm -f $TMPFILE' EXIT TERM INT

[... Continua el script ...]

Esto hará que se borre el fichero temporal que hemos creado, cuando el script acaba (EXIT), se interrumple con Ctrl+C (INT) o se interrumpe de forma amable (con un kill -1, p.e.). Un listado de señales posibles se puede obtener (en bash) de:

$ trap -l

La señal se puede incluir en trap usando el número asociado o el nombre eliminando SIG-. Además de estas señales, hay otros sucesos que pueden ser capturados:

EXIT (también el código 0),

que se produce cuando el programa acaba, bien porque no hay más código bien porque se topa con una orden exit1.

DEBUG (sólo bash),

que se produce justamente antes de que una orden se ejecute.

ERR (sólo bash),

que se produce cada vez que un comando falla.

RETURN (sólo bash),

que se produce cada vez que una función termina su ejecución.

En cuanto a la cadena de órdenes hay dos que tienen significado especial:

  1. La cadena vacía provoca que la señal se ignore. Por ejemplo:

    #!/bin/sh
    
    echo 'Te vas a chupar diez segundos de espera, quieras o no'
    trap '' INT TERM
    sleep 10
    

    Nota

    En realidad, esto no es un significado espacial. Literalmente, se hace lo que hemos dicho, nada, aunque hubiera alguna acción predefinida para esa señal.

  2. La ausencia de cadena o el guión («-«) hacen que la señal vuelva a su comportamiento habitual, o sea, que se anule un trap que previamente hubieramos hecho:

    #!/bin/sh
    
    echo 'Te vas a chupar diez segundos de espera, quieras o no'
    trap '' INT TERM
    sleep 10
    echo 'Pero ahora sí te doy la oportunidad de que te canses de mí antes'
    trap - INT TERM
    sleep 10
    

Advertencia

El efecto no es acomulativo: si redefinimos la acción para una señal, la acción que hubiéramos asociado previamente, ya no se hará. Vea más adelante cómo solvertar esto.

trap también puede usarse sin argumento alguno, en cuyo caso mostrará las acciones definidas para cada señal:

$ trap 'echo "Hola"' INT
$ trap
trap -- 'echo "Hola"' SIGINT

Advertencia

Aunque el estándar POSIX dicta que un trap que se ejecute en una subshell, debe imprimir las acciones definidas en la shell padre:

$ echo $(trap)
trap -- 'echo "Hola"' SIGINT
$ trap | grep '^INT$'
trap -- 'echo "Hola"' SIGINT

dash no lo hace y no devuelve nada2 (sí cumplen el estándar bash, como ilustra el ejemplo, y busybox).

Por último, ya que trap redefine, puede ser interesante disponer de una función que permita acomular acciones ante una señal o evento:

#
# Permite acomular acciones a señales y eventos.
# Uso:
#   trapplus "accion a realizar" SEÑAL1 [SEÑAL2... ]
#
# Las señales deben indicarse con su nombre, no su número.
#
trapplus() {
   local ACTION="$1" pre
   shift

   tmpfile=$(mktemp -u)

   for signal in "$@"; do
      # Para que funcione con dash (que incumple POSIX), hay que montar este sindios.
      { trap ; pre=$(awk '$NF ~ /^(SIG)?'"$signal"'$/ { $NF=""; print }'); rm -f "$tmpfile"; } > "$tmpfile" < "$tmpfile"
      if [ -n "$pre" ]; then
         eval "${pre%\' }; $ACTION' $signal"
      else
         trap -- "$ACTION" "$signal"
      fi
   done
}

Notas al pie

1

En los scripts propuestos para tratar los argumentos hay un ejemplo de cómo mostrar siempre la ayuda cuando el programa acaba por un error en las opciones.

2

Así que si se quiere capturarse la salida, puede usarse esta argucia (la salida queda almacenada en la variable PRE):

{ file($mktemp); trap ; PRE=$(cat); rm -f "$file"; } > "$file" < "$file"