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


Quiero ejecutar un comando de larga duración en Bash, y ambos capturan su estado de salida, y tee su salida.

Así que hago esto:

command | tee out.txt
ST=$?

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

Tenga en cuenta que el comando se está ejecutando durante mucho tiempo y redirigir la salida a un archivo para verlo más tarde no es una buena solución para mí.

Author: codeforester, 2009-08-03

15 answers

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

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

U otra alternativa que también funciona con otros shells (como zsh) sería habilitar pipefail:

set -o pipefail
...

La primera opción hace no trabajar con zsh debido a una sintaxis un poco diferente.

 431
Author: cODAR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-21 17:17:14

Usar bash set -o pipefail es útil

Pipefail: el valor de retorno de una canalización es el estado de el último comando en salir con un estado distinto de cero, o cero si no sale ningún comando con un estado distinto de cero

 127
Author: Felipe Alvarez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-04 08:14:11

Solución tonta: Conectarlos 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 $?
 99
Author: EFraim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-14 15:26:39

Hay una matriz que le da 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
 34
Author: Stefano Borini,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-30 08:41:45

Esta solución funciona sin usar características específicas de bash o archivos temporales. Bonus: 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 someprog y la salida de filter.

Aquí está mi solución:

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

echo $?

Ver mi respuesta a la misma pregunta sobre unix.stackexchange.com para una explicación detallada de cómo funciona eso y algunas advertencias.

 21
Author: lesmana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-10 13:27:29

Al combinar PIPESTATUS[0] y el resultado de ejecutar el comando exit en una subcapa, puede acceder directamente al valor devuelto 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 dará:

return value: 1

 17
Author: par,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-18 03:56:19

Así que quería contribuir con una respuesta como la de lesmana, pero creo que la mía es quizás un poco más simple y ligeramente más ventajosa solución pure-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 desde el inside out - command1 ejecutará e imprimirá su salida regular en stdout (descriptor de archivo 1), luego una vez hecho, printf ejecutará e imprimirá el código de salida de icommand1 en su salida estándar, pero esa salida estándar se redirige al descriptor de archivo 3.

Mientras se ejecuta command1, su salida stdout está siendo canalizada 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 redireccionamos la salida de command2 al descriptor de archivo 4, de modo que también permanezca fuera del descriptor de archivo 1 - porque queremos que el descriptor de archivo 1 sea libre un poco más tarde, porque traeremos la salida de printf en el descriptor de archivo 3 de vuelta al descriptor de archivo 1-porque eso es lo que la sustitución de captura y eso es lo que se colocará en la variable.

La última parte de la magia es que primero exec 4>&1 lo hicimos como un comando separado: abre el descriptor de archivo 4 como una copia de la salida estándar del shell externo. La sustitución de comandos capturará lo que esté escrito en standard desde la perspectiva de los comandos dentro de él-pero como la salida de command2 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 de comandos, sigue yendo al descriptor de archivo general 1 del script.

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

Puedes mirarlo de una manera menos técnica y más lúdica, como si el las salidas de los comandos se están saltando entre sí: command1 canaliza a command2, luego la salida del printf salta sobre el comando 2 para que command2 no lo atrape, y luego la salida del comando 2 salta sobre y fuera de la sustitución del 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 sigue su feliz camino siendo escrita a la salida estándar, al igual que en un tubo normal.

También, según lo entiendo, $? todavía contendrá 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 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 esto podría ser una solución algo mejor que la propuesta por lesmana.

Según las advertencias que lesmana menciona, es posible que command1 en algún momento terminará usando descriptores de archivo 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 subcells (usar ( ) en lugar de { } también funcionará, aunque quizás sea menos eficiente.)

Los comandos

Heredan los descriptores de archivo del proceso que los lanza, 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. Así que el 4>&- se asegura de que el comando compuesto interno no heredará el descriptor de archivo cuatro, y el 3>&- no heredará el descriptor de archivo tres, por lo que el comando 1 obtiene un entorno 'más limpio', más estándar. También puedes mover el interior 4>&- junto al 3>&-, pero me imagino por qué no limitar su alcance tanto como sea posible.

No estoy seguro de con qué frecuencia las cosas usan los descriptores de archivo tres y cuatro directamente-creo que la mayoría de las veces los programas usan llamadas de sistema que devuelven no usadas en el momento descriptores de archivo, pero a veces el código escribe en el descriptor de archivo 3 directamente, supongo (podría imaginar un programa comprobando 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, este último es probablemente el mejor para tener en cuenta y usar para casos de propósito general.

 8
Author: mtraceur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-13 06:33:11

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

 4
Author: Bryan Larsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-13 18:33:04

PIPESTATUS[@] debe copiarse en un array inmediatamente después de que el comando pipe regrese. Cualquier lectura de PIPESTATUS [@] borrará el contenido. Cópielo a otra matriz si planea verificar el estado de todos los comandos de tubería. "$?"es el mismo valor que el último elemento de "${PIPESTATUS[@]}", y leerlo parece destruir " PIP {PIPESTATUS [@]}", pero no he verificado absolutamente esto.

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

Esto no funcionará si la tubería está en un sub-shell. Para una solución a eso problema,
ver bash pipestatus in backticked command?

 3
Author: maxdev137,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:59
(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 éxito y 127 para error. Pero como @ Chaoran señaló, solo puedes llamar ${PIPESTATUS[0]}. Sin embargo, es importante que todo se ponga entre paréntesis.

 3
Author: jakob-r,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-25 08:42:27

Fuera de bash, puedes hacer:

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

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

 2
Author: Anthony Scemama,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-17 18:53:38

La forma más sencilla de hacer esto en bash simple es usar sustitución de procesos en lugar de una canalización. Hay varias diferencias, pero probablemente no importan mucho para su caso de uso:

  • Cuando se ejecuta una canalizació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 el principal.
  • La opción pipefail y la variable PIPESTATUS son irrelevantes para procesar sustitución.
  • Posiblemente más

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

Diferencias mencionadas aparte, consumer < <(producer) y producer | consumer son esencialmente equivalentes.

Si desea voltear cuál es el proceso "principal", simplemente voltee los comandos y la dirección de la sustitución a producer > >(consumer). En su 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 he dicho, hay diferencias de la expresión de tubo. 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 en su salida estándar, lo que puede ser confuso.

 2
Author: clacke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-15 07:09:44

Solución de cáscara 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 salida estándar se cierra en él. 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 pueda volcarlo también en un archivo de registro si se produce un error, o simplemente eliminarlo si no hay error (como la salida de dd).

 1
Author: Coroos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-31 10:08:58

Basándonos en la respuesta de @brian-s-wilson; esta función auxiliar bash:

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 salida; pero queremos ver salida que sí produce

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

2: todo pipeline debe tener éxito

thing | something -q | thingy
pipeinfo || return
 1
Author: Sam Liddicott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-15 15:29:41

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

$ # 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 con las tuberías bash nativas que la sustitución de procesos bash utilizada en answer #43972501.

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

 1
Author: clacke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-15 07:26:41