Salida de tubería y estado de salida de captura en Bash

Quiero ejecutar un comando de larga ejecución en Bash, y ambos capturan su estado de salida y dan salida a la salida.

Entonces hago esto:

command | tee out.txt ST=$? 

El problema es que la variable ST captura el estado de tee de tee y no de comando. ¿Como puedo resolver esto?

Tenga en cuenta que el comando es de larga ejecución y redirigir el resultado a un archivo para verlo más tarde no es una buena solución para mí.

Hay una variable Bash interna llamada $PIPESTATUS ; es una matriz que contiene el estado de salida de cada comando en su última línea de comandos en primer plano.

  | tee out.txt ; test ${PIPESTATUS[0]} -eq 0 

Otra alternativa que también funciona con otras shells (como zsh) sería habilitar pipefail:

 set -o pipefail ... 

La primera opción no funciona con zsh debido a una syntax un poco diferente.

usar el set -o pipefail de bash set -o pipefail es útil

pipefail: el valor de retorno de una tubería es el estado de la última orden para salir con un estado distinto de cero, o cero si no salió ningún comando con un estado distinto de cero

Solución tonta: conectándolas a través de una tubería con nombre (mkfifo). Entonces el comando se puede ejecutar en segundo lugar.

  mkfifo pipe tee out.txt < pipe & command > pipe echo $? 

Hay una matriz que le proporciona el estado de salida de cada comando en una tubería.

 $ cat x| sed 's///' cat: x: No such file or directory $ echo $? 0 $ cat x| sed 's///' cat: x: No such file or directory $ echo ${PIPESTATUS[*]} 1 0 $ touch x $ cat x| sed 's' sed: 1: "s": substitute pattern can not be delimited by newline or backslash $ echo ${PIPESTATUS[*]} 0 1 

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 

quieres el estado de salida de someprog y la salida del filter .

Aquí está mi solución:

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

Consulte mi respuesta para la misma pregunta en unix.stackexchange.com para obtener una explicación detallada de cómo funciona y algunas advertencias.

Al combinar PIPESTATUS[0] y el resultado de ejecutar el comando de exit en una subshell, puede acceder directamente al valor de retorno de su comando inicial:

command | tee ; ( exit ${PIPESTATUS[0]} )

Aquí hay un ejemplo:

 # the "false" shell built-in command returns 1 false | tee ; ( exit ${PIPESTATUS[0]} ) echo "return value: $?" 

Te regalaré:

return value: 1

Así que quería aportar una respuesta como la de Lesmana, pero creo que la mía es quizás una solución de shell Bourne pura algo más simple y ligeramente más ventajosa:

 # 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 desde adentro hacia afuera – command1 ejecutará e imprimirá su salida regular en stdout (descriptor de archivo 1), y una vez hecho, printf ejecutará e imprimirá el código de salida de icommand1 en su stdout, pero ese stdout se redirigirá a descriptor de archivo 3.

Mientras command1 se está ejecutando, su stdout se canaliza a command2 (el resultado de printf nunca llega a command2 porque lo enviamos al descriptor de archivo 3 en lugar de 1, que es lo que lee el conducto). Luego redirigimos la salida de command2 al descriptor de archivo 4, para que también permanezca fuera del descriptor de archivo 1 – porque queremos el descriptor de archivo 1 gratis un poco más tarde, porque llevaremos la salida printf en el descriptor 3 de archivo al descriptor de archivo 1 – porque eso es lo que capturará el comando (los backticks), y eso es lo que se colocará en la variable.

La última parte de la magia es que el primer exec 4>&1 lo 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á lo que está escrito en la salida estándar desde la perspectiva de los comandos dentro de ella, pero como la salida de command2 va a presentar el descriptor 4 en lo que respecta a la sustitución de comandos, la sustitución de comandos no lo captura, aunque una vez se “sale” de la sustitución del comando, de hecho continúa yendo al descriptor general del archivo del script 1.

(El exec 4>&1 tiene que ser un comando separado porque a muchas capas comunes no les gusta cuando intenta 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 esta es la manera portátil más simple de hacerlo).

Puedes mirarlo de una manera menos técnica y más lúdica, como si las salidas de los comandos se saltaran entre sí: command1 pipes a command2, luego la salida de printf salta sobre el comando 2 para que command2 no la atrape, y luego La salida del comando 2 salta una y otra vez de la sustitución de comando justo cuando printf aterriza justo a tiempo para ser capturado por la sustitución para que termine en la variable, y la salida de command2 se graba de manera feliz en la salida estándar, al igual que en una tubería normal.

Además, como yo lo entiendo, $? 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 todos transparentes para el 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 por lo que creo que esta podría ser una solución algo mejor que la propuesta por lesmana.

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

 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 las subcapas (usando ( ) lugar de { } también funcionarán, aunque quizás sean menos eficientes).

Los comandos heredan 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 de 3>&1 heredará el descriptor de archivo tres. Entonces, el 4>&- se asegura de que el comando compuesto interno no herede el descriptor de archivo cuatro, y el 3>&- no heredará el descriptor de archivo tres, por lo que command1 obtiene un entorno más “limpio”, más estándar. También puede mover el 4>&- interno junto al 3>&- , pero me imagino por qué no solo limita su scope tanto como sea posible.

No estoy seguro de la frecuencia con la que las cosas usan el descriptor de archivo tres y cuatro directamente. Creo que la mayoría de las veces los progtwigs usan syscalls que devuelven descriptores de archivos no utilizados en el momento, pero a veces codifican directamente en el descriptor de archivo 3, adivinar (podría imaginar un progtwig que verifica un descriptor de archivo para ver si está abierto, y si lo está usando, o si se comporta de manera diferente si no lo está). Por lo tanto, es probable que lo último sea mejor tenerlo en cuenta y usarlo para casos de propósito general.

En Ubuntu y Debian, puedes apt-get install moreutils . Esto contiene una utilidad llamada mispipe que devuelve el estado de salida del primer comando en la tubería.

PIPESTATUS [@] debe copiarse en una matriz inmediatamente después de que el comando de tubería regrese. Cualquier lectura de PIPESTATUS [@] borrará los contenidos. Cópielo en otra matriz si planea verificar el estado de todos los comandos de la tubería. ps es el mismo valor que el último elemento de “$ {PIPESTATUS [@]}”, y al leerlo parece destruir “$ {PIPESTATUS [@]}”, pero no lo he verificado del todo.

 declare -a PSA cmd1 | cmd2 | cmd3 PSA=( "${PIPESTATUS[@]}" ) 

Esto no funcionará si la tubería está en un subconjunto. Para una solución a ese problema,
¿Ves bash pipestatus en comando revertido?

 (command | tee out.txt; exit ${PIPESTATUS[0]}) 

A diferencia de la respuesta de @CODAR, esto devuelve el código de salida original del primer comando y no solo 0 para el éxito y 127 para el fracaso. Pero como señaló @Chaoran, simplemente puede llamar ${PIPESTATUS[0]} . Sin embargo, es importante que todo esté entre corchetes.

Fuera de bash, puedes hacer:

 bash -o pipefail -c "command1 | tee output" 

Esto es útil, por ejemplo, en los scripts ninja donde se espera que el shell sea /bin/sh .

La forma más sencilla de hacer esto en plain bash es utilizar la sustitución de procesos en lugar de una tubería. Hay varias diferencias, pero probablemente no importan demasiado para su caso de uso:

  • Cuando se ejecuta una interconexión, bash espera hasta que se completen todos los procesos.
  • Enviar Ctrl-C a bash hace que mate todos los procesos de una canalización, no solo la principal.
  • La opción pipefail y la variable PIPESTATUS son irrelevantes para la sustitución del proceso.
  • Posiblemente más

Con la sustitución de procesos, bash solo inicia el proceso y se olvida de él, ni siquiera es visible en los jobs .

Aparte de las diferencias mencionadas, el consumer < <(producer) y el producer | consumer producer | consumer es esencialmente equivalente.

Si desea voltear cuál es el proceso "principal", simplemente voltea los comandos y la dirección de la sustitución al producer > >(consumer) . En tu caso:

 command > >(tee out.txt) 

Ejemplo:

 $ { echo "hello world"; false; } > >(tee out.txt) hello world $ echo $? 1 $ cat out.txt hello world $ echo "hello world" > >(tee out.txt) hello world $ echo $? 0 $ cat out.txt hello world 

Como dije, hay diferencias con la expresión de la tubería. Es posible que el proceso nunca deje de ejecutarse, a menos que sea sensible al cierre de la tubería. En particular, puede seguir escribiendo cosas para su stdout, lo que puede ser confuso.

Solución de concha pura:

 % rm -f error.flag; echo hello world \ | (cat || echo "First command failed: $?" >> error.flag) \ | (cat || echo "Second command failed: $?" >> error.flag) \ | (cat || echo "Third command failed: $?" >> error.flag) \ ; test -s error.flag && (echo Some command failed: ; cat error.flag) hello world 

Y ahora con el segundo cat reemplazado por false :

 % rm -f error.flag; echo hello world \ | (cat || echo "First command failed: $?" >> error.flag) \ | (false || echo "Second command failed: $?" >> error.flag) \ | (cat || echo "Third command failed: $?" >> error.flag) \ ; test -s error.flag && (echo Some command failed: ; cat error.flag) Some command failed: Second command failed: 1 First command failed: 141 

Tenga en cuenta que el primer gato también falla, porque su stdout se cierra. El orden de los comandos fallidos en el registro es correcto en este ejemplo, pero no confíe en él.

Este método permite capturar stdout y stderr para los comandos individuales para que luego pueda volcar también en un archivo de registro si se produce un error, o simplemente eliminarlo si no hay error (como el resultado de dd).

Base en la respuesta de @brian-s-wilson; esta función bash helper:

 pipestatus() { local S=("${PIPESTATUS[@]}") if test -n "$*" then test "$*" = "${S[*]}" else ! [[ "${S[@]}" =~ [^0\ ] ]] fi } 

usado así:

1: get_bad_things debe tener éxito, pero no debe producir ningún resultado; pero queremos ver resultados que produzca

 get_bad_things | grep '^' pipeinfo 0 1 || return 

2: toda la tubería debe tener éxito

 thing | something -q | thingy pipeinfo || return 

A veces puede ser más simple y claro usar un comando externo, en lugar de profundizar en los detalles de bash. pipeline , de la secuencia de comandos mínima del lenguaje de progtwigción , sale con el código de retorno del segundo comando *, al igual que hace sh pipe, pero a diferencia de sh , permite invertir la dirección del conducto, de modo que podemos capturar el código de retorno de el proceso del productor (el siguiente está en la línea de comando sh , pero con la execline línea execline ):

 $ # using the full execline grammar with the execlineb parser: $ execlineb -c 'pipeline { echo "hello world" } tee out.txt' hello world $ cat out.txt hello world $ # for these simple examples, one can forego the parser and just use "" as a separator $ # traditional order $ pipeline echo "hello world" "" tee out.txt hello world $ # "write" order (second command writes rather than reads) $ pipeline -w tee out.txt "" echo "hello world" hello world $ # pipeline execs into the second command, so that's the RC we get $ pipeline -w tee out.txt "" false; echo $? 1 $ pipeline -w tee out.txt "" true; echo $? 0 $ # output and exit status $ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?" hello world RC: 42 $ cat out.txt hello world 

El uso de pipeline tiene las mismas diferencias que las tuberías de bash nativas como la sustitución del proceso de bash utilizada en la respuesta # 43972501 .

* En realidad, el pipeline no sale en absoluto a menos que haya un error. Se ejecuta en el segundo comando, por lo que es el segundo comando el que regresa.