3.6.9. Código de salida en tuberías

Cuando la shell ejecuta una tubería:

orden1 | orden2

el código de salida (el que podemos consultar con $?) es siempre el código de la orden derecha:

$ true | false
$ echo $?
1
$ false | true
$ echo $?
0

Que no podamos acceder al código de salida de la primera orden puede suponernos, en ocasiones, un problema. Por ejemplo, supongamos que creamos dos funciones: una se encarga de procesar datos y generar los resultados (o sea, se encarga de la lógica del problema), mientras que la otra, simplemente, coge los resultados y los formatea adecuadamente. Por ejemplo, supongamos que creamos una función a la que le damos un rango de números y nos devuelve los caracteres imprimibles correspondientes, suponiendo que los números son códigos ASCII:

# Obtiene los caracteres imprimibles correspondientes
# a un rango de códigos ASCII
get_chars() {
   local ini="$1" fin="${2:-255}"

   # Posibles errores.
   [ $ini -le $fin ] || return 1  # Rango invertido.
   [ $ini -ge 32 -a $fin -le 255 ] || return 1  # Valores fuera de rango.

   for i in $(seq $ini $fin); do
      /usr/bin/printf "%d \x$(printf %x $i)\n" $i
   done
}

que al ejecutarse con argumentos 60 y 65 devuelve esto:

60 <
61 =
62 >
63 ?
64 @
65 A

La función puede fallar, si incluimos múmeros fuera del rango 32-255 que son los caracteres imprimibles. Esta función, no obstante, no se encarga de crear una salida bonita, sólo de resolver el problema en sí. Podríamos hacer que la propia función formateara, pero supongamos que preferimos hacerlo en una función aparte para separar la lógica de la presentación. En consecuencia creamos esta función:

formatea() {
   while read -r code char; do
      printf "%.25s %s\n" "$code......................................." "$char"
   done
}

que obrará el cambio: al ejecutarse así:

get_chars 60 65 | formatea

de modo que generará una salida más agradable:

60....................... <
61....................... =
62....................... >
63....................... ?
64....................... @
65....................... A

El problema de esta solución es que si en el programa en que incluimos estas funciones, queremos saber si la operación falló, nos será imposible puesto que la segunda función siempre devolverá éxito, incluso cuando no tenga nada que formatear.

bash soluciona de forma muy sencilla esto de dos formas:

  1. Consultando el array PIPESTATUS que almacena los estados de todos los programas que intervienen en la tubería. En el ejemplo ${PIPESTATUS[0]}, almacena el código de salida de get_chars y ${PIPESTATUS[1]} el de formatea.

  2. Usando la opción pipefail:

    set -o pipefail
    

    que provoca que se devuelva el código de salida del programa más a la derecha que falló[1] y sólo se devuelva 0, si no falló ningún programa.

Pero estas facilidades no existen en el estándar POSIX. Por ello, hay que buscarle las vueltas al problema. Una solución no demasiado compleja es la siguiente[2]:

{ { { get_chars 60 65 3>&- 4>&-; echo $? >&3; } | formatea >&4; } 3>&1 | { read XC; exit $XC; } } 4>&1

que consiste en jugar con las redirecciones para lograr que el código de salida de get_chars sea leído por la orden read. Como este read está en el último miembro de la tubería, y salimos de él con el código leído, logramos que el código de salida resultante sea el código de salida del miembro izquierdo.

Como es un poco engorroso contruir la expresión y, además, se ofusca mucho la tubería, podemos crear una función que equivalga a lo anterior:

#
# En una tubería simple de dos meimbros devuelve
# el código de salida de la orden izquierda.
#
pf() {
   local XC run

   while [ $# -gt 0 ]; do
      if [ "$1" = "|" ]; then
         run="$run "'3>&- 4>&-; echo $? >&3; } '
         break
      else
         run="$run '$1'"
      fi
      shift
   done

   eval "{ { { " $run "$@" '>&4; } 3>&1 | { read XC; return $XC; } } 4>&1'
}

Con ella podemos ejecutar así la tubería:

pf get_chars 60 65 \| formatea

Nota

Observe que hay que proteger la tubería para que la shell no la interprete.

Notas al pie