Obtener el estado de salida del proceso que se canaliza a otro


Respuestas:


256

Si está usando bash, puede usar la PIPESTATUSvariable de matriz para obtener el estado de salida de cada elemento de la tubería.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Si está utilizando zsh, se llama a la matriz pipestatus(¡el caso importa!) Y los índices de la matriz comienzan en uno:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Para combinarlos dentro de una función de una manera que no pierda los valores:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Ejecute lo anterior en basho zshy obtendrá los mismos resultados; solo se establecerá uno de retval_bashy retval_zsh. El otro estará en blanco. Esto permitiría que una función terminara return $retval_bash $retval_zsh(¡tenga en cuenta la falta de comillas!).


99
Y pipestatusen zsh. Lamentablemente, otros proyectiles no tienen esta característica.
Gilles

8
Nota: Las matrices en zsh comienzan contraintuitivamente en el índice 1, por lo que es echo "$pipestatus[1]" "$pipestatus[2]".
Christoph Wurm

55
Puede verificar toda la tubería de esta manera:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

77
@ JanHudec: Quizás deberías leer las primeras cinco palabras de mi respuesta. También indique amablemente dónde la pregunta solicitó una respuesta solo POSIX.
camh

12
@ JanHudec: Tampoco fue etiquetado POSIX. ¿Por qué supone que la respuesta debe ser POSIX? No se especificó, así que proporcioné una respuesta calificada. No hay nada incorrecto en mi respuesta, además hay varias respuestas para abordar otros casos.
camh

238

Hay 3 formas comunes de hacer esto:

Pipefail

La primera forma es establecer la pipefailopción ( ksh, zsho bash). Este es el más simple y lo que hace es básicamente establecer el estado $?de salida en el código de salida del último programa para salir de cero (o cero si todos salieron con éxito).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATO

Bash también tiene una variable de matriz llamada $PIPESTATUS( $pipestatusin zsh) que contiene el estado de salida de todos los programas en la última canalización.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

Puede usar el tercer ejemplo de comando para obtener el valor específico en la tubería que necesita.

Ejecuciones separadas

Esta es la más difícil de manejar de las soluciones. Ejecute cada comando por separado y capture el estado

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

2
¡Maldito! Solo iba a publicar sobre PIPESTATUS.
slm

1
Como referencia, hay varias otras técnicas discutidas en esta pregunta SO: stackoverflow.com/questions/1221833/…
slm

1
@Patrick la solución pipestatus funciona en bash, solo más quastion en caso de que use el script ksh, ¿crees que podemos encontrar algo similar a pipestatus? , (mientras tanto veo el pipestatus no soportado por ksh)
yael

2
@yael no lo uso ksh, pero de un breve vistazo a su página de manual, no es compatible $PIPESTATUSni nada similar. Sin embargo, admite la pipefailopción.
Patrick

1
Decidí ir con pipefail ya que me permite obtener el estado del comando fallido aquí:LOG=$(failed_command | successful_command)
vmrob

51

Esta solución funciona sin usar funciones específicas de bash o archivos temporales. Bonificación: al final, el estado de salida es en realidad un estado de salida y no una cadena en un archivo.

Situación:

someprog | filter

desea el estado de salida de someprogy la salida de filter.

Aquí está mi solución:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

el resultado de esta construcción es stdout desde filterstdout de la construcción y el estado de salida desde someprogcomo estado de salida de la construcción.


Esta construcción también funciona con la agrupación de comandos simple en {...}lugar de subcapas (...). Las subcapas tienen algunas implicaciones, entre otras, un costo de rendimiento, que no necesitamos aquí. lea el manual de fine bash para obtener más detalles: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Desafortunadamente, la gramática bash requiere espacios y puntos y comas para las llaves para que la construcción se vuelva mucho más espaciosa.

Para el resto de este texto, usaré la variante subshell.


Ejemplo someprogy filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Salida de ejemplo:

filtered line1
filtered line2
filtered line3
42

Nota: el proceso hijo hereda los descriptores de archivo abiertos del padre. Eso significa someprogque heredará el descriptor de archivo abierto 3 y 4. Si someprogescribe en el descriptor de archivo 3, se convertirá en el estado de salida. El estado de salida real se ignorará porque readsolo se lee una vez.

Si le preocupa que someprogpueda escribir en el descriptor de archivo 3 o 4, entonces es mejor cerrar los descriptores de archivo antes de llamar someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

El exec 3>&- 4>&-antes someprogcierra el descriptor de archivo antes de ejecutarlo, someprogpor lo que para someprogesos descriptores de archivo simplemente no existen.

También se puede escribir así: someprog 3>&- 4>&-


Explicación paso a paso de la construcción:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

De abajo hacia arriba:

  1. Se crea una subshell con el descriptor de archivo 4 redirigido a stdout. Esto significa que todo lo que se imprima en el descriptor de archivo 4 en la subshell terminará como la salida estándar de toda la construcción.
  2. Se crea una tubería y se ejecutan los comandos de la izquierda ( #part3) y la derecha ( #part2). exit $xstambién es el último comando de la tubería y eso significa que la cadena de stdin será el estado de salida de toda la construcción.
  3. Se crea una subshell con el descriptor de archivo 3 redirigido a stdout. Esto significa que todo lo que se imprima en el descriptor de archivo 3 en esta subshell terminará #part2y, a su vez, será el estado de salida de toda la construcción.
  4. Se crea una tubería y se ejecutan los comandos a la izquierda ( #part5y #part6) y a la derecha ( filter >&4). La salida de filterse redirige al descriptor de archivo 4. En #part1el descriptor de archivo 4 se redirige a stdout. Esto significa que la salida de filteres la salida estándar de toda la construcción.
  5. El estado de salida de #part6se imprime en el descriptor de archivo 3. En #part3el descriptor de archivo 3 se redirige a #part2. Esto significa que el estado de salida de #part6será el estado de salida final para toda la construcción.
  6. someproges ejecutado. Se toma el estado de salida #part5. La tubería toma el stdout #part4y lo reenvía filter. La salida de filtera su vez alcanzará stdout como se explica en#part4

1
Agradable. Para la función que podría hacer(read; exit $REPLY)
jthill

55
(exec 3>&- 4>&-; someprog)simplifica a someprog 3>&- 4>&-.
Roger Pate

Este método también funciona sin subcapas:{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
josch

36

Si bien no es exactamente lo que pediste, podrías usar

#!/bin/bash -o pipefail

para que sus tuberías devuelvan el último retorno distinto de cero.

podría ser un poco menos codificación

Editar: Ejemplo

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1

99
set -o pipefaildentro del script debe ser más robusto, por ejemplo, en caso de que alguien ejecute el script a través de bash foo.sh.
maxschlepzig

¿Cómo funciona? tienes un ejemplo
Johan

2
Tenga en cuenta que -o pipefailno está en POSIX.
scy

2
Esto no funciona en mi versión BASH 3.2.25 (1). En la parte superior de / tmp / ff tengo #!/bin/bash -o pipefail. El error es:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez

2
@FelipeAlvarez: Algunos entornos (incluyendo Linux) no analizar espacios en #!líneas más allá de la primera, y por lo que este se convierte /bin/bash -o pipefail /tmp/ff, en lugar de lo necesario /bin/bash -o pipefail /tmp/ff- getoptel análisis sintáctico (o similar) con el optarg, que es el siguiente elemento ARGV, como el argumento a -o, entonces falla. Si tuviera que hacer una envoltura (digamos, bash-pfeso acaba de hacer exec /bin/bash -o pipefail "$@", y poner eso en la #!línea, eso funcionaría. Ver también: en.wikipedia.org/wiki/Shebang_%28Unix%29
lindes

22

Lo que hago cuando sea posible es alimentar el código de salida desde foodentro bar. Por ejemplo, si sé que foonunca produce una línea con solo dígitos, entonces puedo agregar el código de salida:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

O si sé que la salida de foonunca contiene una línea con solo .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Esto siempre se puede hacer si hay alguna forma de llegar bara trabajar en todos, excepto en la última línea, y pasar la última línea como su código de salida.

Si se bartrata de una tubería compleja cuya salida no necesita, puede omitir parte de ella imprimiendo el código de salida en un descriptor de archivo diferente.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

Después de esto $exit_codeses generalmente foo:X bar:Y, pero podría ser bar:Y foo:Xsi se barcierra antes de leer toda su entrada o si tiene mala suerte. Creo que las escrituras en las tuberías de hasta 512 bytes son atómicas en todos los sistemas Unix, por lo que los foo:$?y bar:$?las piezas no se pueden mezclar, siempre y cuando las cuerdas están bajo la etiqueta de 507 bytes.

Si necesita capturar la salida bar, se hace difícil. Puede combinar las técnicas anteriores organizando la salida de barnunca para que contenga una línea que parezca una indicación de código de salida, pero se vuelve complicada.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

Y, por supuesto, existe la opción simple de usar un archivo temporal para almacenar el estado. Simple, pero no tan simple en producción:

  • Si hay varias secuencias de comandos ejecutándose simultáneamente, o si la misma secuencia de comandos usa este método en varios lugares, debe asegurarse de que usen diferentes nombres de archivos temporales.
  • Crear un archivo temporal de forma segura en un directorio compartido es difícil. A menudo, /tmpes el único lugar donde un script seguramente podrá escribir archivos. Uso mktemp, que no es POSIX pero está disponible en todas las unidades serias hoy en día.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

1
Cuando se utiliza el enfoque de archivo temporal prefiero añadir una trampa para la salida que elimina todos los archivos temporales para que la basura no se dejará incluso si el guión muere
miracle173

17

A partir de la tubería:

foo | bar | baz

Aquí hay una solución general que usa solo shell POSIX y no archivos temporales:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses contiene los códigos de estado de cualquier proceso fallido, en orden aleatorio, con índices para indicar qué comando emitió cada estado.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Tenga en cuenta las citas $error_statusesen mis pruebas; sin ellos grepno se puede diferenciar porque las nuevas líneas se convierten en espacios forzados.


12

Así que quería aportar una respuesta como la de lesmana, pero creo que la mía es quizás una solución un poco más simple y un poco más ventajosa de Bourne-shell:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Creo que esto se explica mejor de adentro hacia afuera: command1 se ejecutará e imprimirá su salida regular en stdout (descriptor de archivo 1), luego, una vez hecho, printf se ejecutará e imprimirá el código de salida de command1 en su stdout, pero ese stdout se redirige a descriptor de archivo 3.

Mientras se ejecuta command1, su stdout se canaliza a command2 (la salida de printf nunca llega a command2 porque lo enviamos al descriptor de archivo 3 en lugar de 1, que es lo que lee la tubería). Luego redirigimos la salida del comando 2 al descriptor de archivo 4, de modo que también quede fuera del descriptor de archivo 1, porque queremos que el descriptor de archivo 1 esté libre un poco más tarde, porque volveremos a colocar la salida de printf en el descriptor de archivo 3 en el descriptor de archivo 1 - porque eso es lo que capturará la sustitución del comando (los backticks) y eso es lo que se colocará en la variable.

La última parte de la magia es que primero exec 4>&1lo hicimos como un comando separado: abre el descriptor de archivo 4 como una copia del stdout del shell externo. La sustitución de comandos capturará todo lo que está escrito en el estándar desde la perspectiva de los comandos dentro de él, pero, dado que la salida del comando2 va al descriptor de archivo 4 en lo que respecta a la sustitución de comandos, la sustitución de comandos no lo captura, sin embargo, una vez que se "sale" de la sustitución del comando, efectivamente sigue yendo al descriptor de archivo general 1 del script.

( exec 4>&1Tiene que ser un comando separado porque a muchos shells comunes no les gusta cuando intentas escribir en un descriptor de archivo dentro de una sustitución de comando, que se abre en el comando "externo" que está usando la sustitución. Así que este es el La forma portátil más sencilla de hacerlo).

Puede verlo de una manera menos técnica y más lúdica, como si las salidas de los comandos se saltaran entre sí: command1 se canaliza hacia command2, luego la salida de printf salta sobre el comando 2 para que command2 no lo atrape, y luego La salida del comando 2 salta y sale de la sustitución del comando justo cuando printf aterriza justo a tiempo para ser capturado por la sustitución de modo que termine en la variable, y la salida del comando 2 se escribe alegremente en la salida estándar, tal como en una tubería normal

Además, según tengo entendido, $?seguirá conteniendo el código de retorno del segundo comando en la tubería, porque las asignaciones de variables, las sustituciones de comandos y los comandos compuestos son efectivamente transparentes al código de retorno del comando dentro de ellos, por lo que el estado de retorno de command2 debería propagarse; esto, y no tener que definir una función adicional, es la razón por la que creo que esta podría ser una solución algo mejor que la propuesta por lesmana.

Según las advertencias que menciona lesmana, es posible que command1 en algún momento termine usando los descriptores de archivo 3 o 4, por lo que para ser más robusto, haría lo siguiente:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Tenga en cuenta que uso comandos compuestos en mi ejemplo, pero subcapas (usar en ( )lugar de { }también funcionará, aunque tal vez sea menos eficiente).

Los comandos heredan los descriptores de archivo del proceso que los inicia, por lo que toda la segunda línea heredará el descriptor de archivo cuatro, y el comando compuesto seguido 3>&1heredará el descriptor de archivo tres. Por lo tanto, 4>&-se asegura de que el comando compuesto interno no heredará el descriptor de archivo cuatro, y 3>&-no heredará el descriptor de archivo tres, por lo que command1 obtiene un entorno más limpio y estándar. También puede mover el interior al 4>&-lado del 3>&-, pero me imagino por qué no limitar su alcance tanto como sea posible.

No estoy seguro de con qué frecuencia las cosas usan el descriptor de archivo tres y cuatro directamente; creo que la mayoría de las veces los programas usan syscalls que devuelven descriptores de archivo no utilizados en este momento, pero a veces las escrituras de código en el descriptor de archivo 3 directamente, yo Supongo (podría imaginar un programa revisando un descriptor de archivo para ver si está abierto, y usándolo si lo está, o comportándose de manera diferente si no lo está). Por lo tanto, lo último es probablemente mejor tener en cuenta y usar para casos de uso general.


Parece interesante, pero no puedo entender qué espera que haga este comando, y mi computadora tampoco; Consigo -bash: 3: Bad file descriptor.
G-Man

@ G-Man Bien, sigo olvidando que bash no tiene idea de lo que está haciendo cuando se trata de descriptores de archivos, a diferencia de los shells que uso habitualmente (la ceniza que viene con busybox). Te avisaré cuando piense en una solución que haga feliz a bash. Mientras tanto, si tienes una caja de Debian a mano, puedes probarla en el tablero, o si tienes busybox a mano, puedes probarla con la busybox ash / sh.
mtraceur

@ G-Man En cuanto a lo que espero que haga el comando, y lo que hace en otros shells, es redirigir stdout desde command1 para que no quede atrapado por la sustitución de comando, pero una vez fuera de la sustitución de comando, se cae fd3 vuelve a stdout, por lo que se canaliza como se espera que command2. Cuando sale el comando1, printf se dispara e imprime su estado de salida, que se captura en la variable mediante la sustitución del comando. Desglose muy detallado aquí: stackoverflow.com/questions/985876/tee-and-exit-status/… Además, ese comentario tuyo se lee como si fuera un insulto?
mtraceur

¿Por dónde empezaré? (1) Lo siento si te sentiste insultado. "Parece interesante" significaba con seriedad; sería genial si algo tan compacto como eso funcionara tan bien como esperabas. Más allá de eso, estaba diciendo, simplemente, que no entendía lo que se suponía que su solución debía hacer. He estado trabajando / jugando con Unix durante mucho tiempo (desde antes de que existiera Linux) y, si no entiendo algo, es una señal de alerta que, tal vez, otras personas tampoco lo entenderán, y que necesita más explicación (IMNSHO). … (Continúa)
G-Man el

(Continúa) ... Ya que "... te gusta pensar ... que [entiendes] casi todo más que la persona promedio", tal vez deberías recordar que el objetivo de Stack Exchange no es ser un servicio de redacción de comandos. miles de soluciones únicas a preguntas trivialmente distintas; sino más bien para enseñar a las personas cómo resolver sus propios problemas. Y, con ese fin, tal vez necesite explicar las cosas lo suficientemente bien como para que una "persona promedio" pueda entenderlo. Mire la respuesta de lesmana para ver un ejemplo de una excelente explicación. … (Continúa)
G-Man el


7

La solución anterior de lesmana también se puede hacer sin la sobrecarga de iniciar subprocesos anidados utilizando en su { .. }lugar (recordando que esta forma de comandos agrupados siempre tiene que terminar con punto y coma). Algo como esto:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

He comprobado esta construcción con la versión de guión 0.5.5 y las versiones de bash 3.2.25 y 4.2.42, por lo que incluso si algunos shells no admiten la { .. }agrupación, sigue siendo compatible con POSIX.


Esto funciona muy bien con la mayoría de los shells con los que lo he probado, incluidos NetBSD sh, pdksh, mksh, dash, bash. Sin embargo, no puedo hacer que funcione con AT&T Ksh (93s +, 93u +) o zsh (4.3.9, 5.2), incluso con set -o pipefailin ksh o cualquier número de waitcomandos rociados en cualquiera de ellos. Creo que, en parte, al menos, puede ser un problema de análisis de ksh, ya que si me limito a usar subshells, entonces funciona bien, pero incluso con un ifpara elegir la variante de subshell para ksh pero dejar los comandos compuestos para otros, falla .
Greg A. Woods, el

4

Esto es portátil, es decir, funciona con cualquier shell compatible con POSIX, no requiere que el directorio actual sea editable y permite que se ejecuten simultáneamente varios scripts que usan el mismo truco.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Editar: aquí hay una versión más fuerte después de los comentarios de Gilles:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: y aquí hay una variante ligeramente más ligera después del comentario dudoso de Jim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))

3
Esto no funciona por varias razones. 1. El archivo temporal puede leerse antes de escribirse. 2. Crear un archivo temporal en un directorio compartido con un nombre predecible es inseguro (DoS trivial, carrera de enlace simbólico). 3. Si el mismo script usa este truco varias veces, siempre usará el mismo nombre de archivo. Para resolver 1, lea el archivo después de que la tubería se haya completado. Para resolver 2 y 3, use un archivo temporal con un nombre generado aleatoriamente o en un directorio privado.
Gilles

+1 Bueno, el $ {PIPESTATUS [0]} es más fácil, pero la idea básica aquí funciona si uno conoce los problemas que menciona Gilles.
Johan

Puede guardar un par de subniveles: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Estoy de acuerdo en que es más fácil con Bash, pero en algunos contextos, vale la pena saber cómo evitarlo.
dubiousjim

4

Siguiente se entiende como un complemento a la respuesta de @Patrik, en caso de que no pueda utilizar una de las soluciones comunes.

Esta respuesta supone lo siguiente:

  • Tienes un caparazón que no conoce $PIPESTATUSniset -o pipefail
  • Desea utilizar una tubería para ejecución paralela, por lo que no hay archivos temporales.
  • No desea tener un desorden adicional si interrumpe el script, posiblemente por un apagón repentino.
  • Esta solución debería ser relativamente fácil de seguir y limpia para leer.
  • No desea introducir subcapas adicionales.
  • No puede jugar con los descriptores de archivo existentes, por lo que no debe tocar stdin / out / err (sin embargo, puede introducir uno nuevo temporalmente)

Suposiciones adicionales. Puede deshacerse de todo, pero esto cambia demasiado la receta, por lo que no se trata aquí:

  • Todo lo que quiere saber es que todos los comandos en PIPE tienen el código de salida 0.
  • No necesita información adicional sobre la banda lateral.
  • Su shell espera a que regresen todos los comandos de tubería.

Antes: foo | bar | bazsin embargo, esto solo devuelve el código de salida del último comando ( baz)

Se busca: $?no debe ser 0(verdadero), si alguno de los comandos en la tubería falló

Después:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Explicado:

  • Se crea un archivo temporal con mktemp. Esto generalmente crea inmediatamente un archivo en/tmp
  • Este archivo temporal se redirige a FD 9 para escritura y FD 8 para lectura
  • Luego, el archivo temporal se elimina inmediatamente. Sin embargo, permanece abierto hasta que ambos FD desaparezcan.
  • Ahora se inicia la tubería. Cada paso se agrega solo a FD 9, si hubo un error.
  • Se waitnecesita para ksh, porque de lo kshcontrario no espera a que finalicen todos los comandos de tubería. Sin embargo, tenga en cuenta que hay efectos secundarios no deseados si hay algunas tareas en segundo plano, por lo que lo comenté de forma predeterminada. Si la espera no duele, puedes comentarla.
  • Luego se leen los contenidos del archivo. Si está vacío (porque todo funcionó) readregresa false, entonces trueindica un error

Esto se puede usar como reemplazo de un complemento para un solo comando y solo necesita lo siguiente:

  • FD no utilizados 9 y 8
  • Una única variable de entorno para contener el nombre del archivo temporal
  • Y esta receta se puede adaptar a casi cualquier shell que permita la redirección de IO
  • También es bastante independiente de la plataforma y no necesita cosas como /proc/fd/N

Loco:

Este script tiene un error en caso de que se /tmpquede sin espacio. Si también necesita protección contra este caso artificial, puede hacerlo de la siguiente manera, sin embargo, esto tiene la desventaja de que el número de 0in 000depende del número de comandos en la tubería, por lo que es un poco más complicado:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Notas de portabilidad:

  • kshy los shells similares que solo esperan el último comando de tubería necesitan el waitcomentario no comentado

  • El último ejemplo se usa en printf "%1s" "$?"lugar de echo -n "$?"porque es más portátil. No todas las plataformas interpretan -ncorrectamente.

  • printf "$?"lo haría también, sin embargo, printf "%1s"detecta algunos casos de esquina en caso de que ejecute el script en una plataforma realmente rota. (Lea: si programa en paranoia_mode=extreme).

  • FD 8 y FD 9 pueden ser superiores en plataformas que admiten múltiples dígitos. AFAIR un shell POSIX conforme solo necesita admitir dígitos individuales.

  • Se puso a prueba con Debian 8.2 sh, bash, ksh, ash, sashe inclusocsh


3

Con un poco de precaución, esto debería funcionar:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

¿Qué tal una limpieza como jlliagre? ¿No dejas un archivo llamado foo-status?
Johan

@Johan: Si prefieres mi sugerencia, no dudes en votarla;) Además de no dejar un archivo, tiene la ventaja de permitir que múltiples procesos ejecuten esto simultáneamente y el directorio actual no necesita ser editable.
jlliagre

2

El siguiente bloque 'if' se ejecutará solo si 'command' se realizó correctamente:

if command; then
   # ...
fi

Hablando específicamente, puede ejecutar algo como esto:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Que ejecutará haconf -makerwy almacenará su stdout y stderr en "$ haconf_out". Si el valor devuelto desde haconfes verdadero, entonces el bloque 'if' se ejecutará y grepleerá "$ haconf_out", intentando compararlo con "Cluster ya escribible".

Observe que las tuberías se limpian automáticamente; con la redirección, deberá tener cuidado de eliminar "$ haconf_out" cuando haya terminado.

No es tan elegante como pipefail, pero es una alternativa legítima si esta funcionalidad no está al alcance.


1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$

0

(Con bash al menos) combinado con set -euno puede usar subshell para emular explícitamente pipefail y salir en error de tubería

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Entonces, si foofalla por alguna razón, el resto del programa no se ejecutará y el script se cerrará con el código de error correspondiente. (Esto supone que fooimprime su propio error, que es suficiente para comprender el motivo del fallo)


-1

EDITAR : Esta respuesta es incorrecta, pero interesante, así que la dejaré para referencia futura.


Agregar !a el comando invierte el código de retorno.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #

1
Creo que esto no está relacionado. En su ejemplo, quiero saber el código de salida de ls, no invertir el código de salida debogus_command
Michael Mrozek

2
Sugiero retirar esa respuesta.
maxschlepzig

3
Bueno, parece que soy un idiota. De hecho, he usado esto en un script antes de pensar que hizo lo que quería el OP. Lo bueno es que no lo
usé
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.