2009-08-03 19 views
313

Quiero ejecutar un comando de ejecución larga en Bash, y ambos capturan su estado de salida y tee su salida.Salida de tubería y estado de salida de captura en Bash

por lo que esto:

command | tee out.txt 
ST=$? 

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

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

+1

[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Coincidencia - error encontrado" || echo -e "Sin coincidencia, todo bien" Esto probará todos los valores de la matriz a la vez y dará un mensaje de error si alguno de los valores de la tubería devueltos no son cero. Esta es una solución generalizada bastante robusta para detectar errores en una situación de canalización. –

+0

http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another –

Respuesta

408

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.

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

O otra alternativa que también trabaja con otros proyectiles (como zsh) sería permitir pipefail:

set -o pipefail 
... 

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

+17

Hay una buena explicación con ejemplos de PIPESTATUS Y Pipefail aquí: http://unix.stackexchange.com/a/73180/7453. – slm

+14

Nota: $ PIPESTATUS [0] contiene el estado de salida del primer comando en el conducto, $ PIPESTATUS [1] el estado de salida del segundo comando, y así sucesivamente. – simpleuser

+16

Por supuesto, tenemos que recordar que esto es específico de Bash: si tuviera que (por ejemplo) escribir un script para ejecutar en la implementación "sh" de BusyBox en mi dispositivo Android, o en alguna otra plataforma incorporada usando algún otro "sh "variante, esto no funcionaría. –

87

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 $? 
+7

Esta es la única respuesta en esta pregunta que también funciona para el simple ** sh ** Concha de Unix. ¡Gracias! – JamesThomasMoon1979

+0

¿Por qué es esto tonto? –

+1

@DaveKennedy: tonto como en "obvio, no requiere un conocimiento complejo de la sintaxis bash" – EFraim

33

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 
18

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 que 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 $? 

Ver my answer for the same question on unix.stackexchange.com para una explicación detallada de cómo funciona y algunas advertencias.

16

Al combinar PIPESTATUS[0] y el resultado de la ejecución del comando exit en un subnivel, se puede acceder directamente al valor de retorno de su comando inicial:

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

He aquí un ejemplo:

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

le dará:

return value: 1

+4

Gracias, esto me permitió usar la construcción: '' VALOR = $ (might_fail | piping) '' que no configurará PIPESTATUS en el shell maestro, sino que establecerá su errorlevel. Al usar: '' VALUE = $ (might_fail | piping; exit $ {PIPESTATUS [0]}) '' Me dan ganas de querer. – vaab

+0

@vaab, esa sintaxis se ve muy bien, pero estoy confundido sobre lo que significa 'canalizar' en su contexto? ¿Es justo donde uno haría 'tee' o cualquier procesamiento en la salida de might_fail? ty! – AnneTheAgile

+1

@AnneTheAgile 'tuberías' en mi ejemplo representa comandos de los que no desea ver el error. Por ejemplo: una de las combinaciones de 'tee', 'grep', 'sed', ... No es tan extraño que estos comandos de tuberías tengan el propósito de formatear o extraer información de un resultado de salida o registro más grande de la principal. comando: entonces estás más interesado en el nivel de error del comando principal (el que he llamado 'might_fail' en mi ejemplo) pero sin mi construcción toda la asignación devuelve el último error del comando canalizado que aquí no tiene sentido. ¿Esto es más claro? – vaab

3

PIPESTATUS [@] debe copiarse en una matriz inmediatamente después de que vuelva el comando de tubería. Cualquier lecturas de PIPESTATUS [@] borrará el contenido. Cópielo en 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 parece que destruye "$ {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,
ver bash pipestatus in backticked command?

123

utilizando de set -o pipefail bash es útil

pipefail: el valor de retorno de una tubería es el estado de el último comando para salir con un estado distinto de cero , o cero si no hay ningún comando salido con un estado distinto de cero

+17

En caso de que no desee modificar la configuración de falla de pipeteo de todo el script, puede establecer la opción solo localmente: '(set -o pipefail; command | tee out.txt); ST = $? ' – Jaan

+6

@Jaan Esto ejecutaría una subshell. Si quieres evitar eso, puedes hacer 'set -o pipefail' y luego hacer el comando, e inmediatamente después hacer' set + o pipefail' para desarmar la opción. –

+0

Nota: el cartel de pregunta no quiere un "código de salida general" de la tubería, quiere el código de retorno de 'comando'. Con '-o pipefail' sabría si la tubería falla, pero si fallan tanto 'command' como 'tee', recibiría el código de salida de 'tee'. – t0r0X

3

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

1

solución pura cáscara:

% 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 la segunda 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 el primer gato también falla, porque es la salida estándar se cierra sobre ella. 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 ocurre un error, o simplemente eliminarlo si no hay error (como el resultado de dd).

7

así que quería aportar una respuesta como de Lesmana, pero creo que la mía es tal vez un poco más simple y poco más ventajosa solución de Bourne-Shell pura:

# 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 interior out - command1 ejecutará e imprimirá su salida normal en stdout (descriptor de archivo 1), luego una vez hecho, printf ejecutará e imprimirá el código de salida de icommand1 en su stdout, pero ese stdout se redirigirá al descriptor de archivo 3.

Mientras command1 se está ejecutando, su stdout se canaliza a command2 (la salida de printf nunca llega a command2 beca use lo enviamos al descriptor de archivo 3 en lugar de 1, que es lo que lee la tubería). 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.

El último truco 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 de la salida estándar 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 "sale" de la sustitución del comando, todavía va al descriptor de archivo general del script 1.

(El tiene que ser un comando separado porque a muchos objetos 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 esta es la manera más simple de hacerlo).

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

Además, según tengo entendido, $? aún contendrá el código de retorno del segundo comando en la tubería, porque las asignaciones de variables, sustituciones de comandos y comandos compuestos son efectivamente transparentes para el código de retorno del comando dentro de ellos, por lo tanto, 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 podría ser una solución algo mejor que la propuesta por lesmana.

por las advertencias Lesmana menciona, es posible que comando1 en algún momento terminar usando descriptores de fichero 3 o 4, de modo que sea más robusto, que haría:

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

Nota que utilizo comandos compuestos en mi ejemplo, pero subniveles (usando () en lugar de { } también funcionará, aunque tal vez puede ser menos eficiente.)

comandos heredan descriptores de archivo del proceso que les pone en marcha, por lo que toda la segunda línea heredará descriptor de archivo de cuatro, y el comando compuesto seguido por 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 y más estándar. También puede mover el 4>&- interno junto al 3>&-, pero me imagino por qué no solo limita su alcance tanto como sea posible.

No estoy seguro de la frecuencia con que las cosas usan el descriptor de archivo tres y cuatro directamente - Creo que la mayoría de los programas usan syscalls que devuelven descriptores de archivos no utilizados en el momento, pero a veces escriben códigos en el descriptor de archivo 3 directamente, supongo (podría imaginar un programa 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.

1

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

pipestatus() { 
    local S=("${PIPESTATUS[@]}") 

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

usa así:

1: get_bad_things deben tener éxito, pero debe producir ninguna salida; pero queremos ver una salida que no producen

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

2: toda la tubería debe tener éxito

thing | something -q | thingy 
pipeinfo || return 
2

Fuera de la fiesta, que puede 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.

1

A veces puede ser más simple y claro utilizar un comando externo, en lugar de profundizar en los detalles de bash. pipeline, del lenguaje de scripting de proceso mínimo execline, sale con el código de retorno del segundo comando *, al igual que una tubería sh, pero a diferencia de sh, permite invertir la dirección de la tubería, de modo que podamos capturar el código de retorno de el proceso productor (el 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 

Usando pipeline tiene las mismas diferencias con respecto a las tuberías de bash nativos como la sustitución proceso de bash utilizado en respuesta #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 el que regresa.

2

La forma más sencilla de hacer esto en plain bash es usar process substitution en lugar de una tubería. Hay varias diferencias, pero probablemente no importan mucho para su caso de uso:

  • Cuando se ejecuta una interconexión, bash espera hasta que se completen todos los procesos.
  • El envío de 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, fiesta apenas comienza el proceso y se olvida de él, ni siquiera es visible en jobs.

Las diferencias mencionadas a un lado, consumer < <(producer) y producer | consumer son esencialmente equivalentes.

Si quiere 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 ya he dicho, hay diferencias con respecto a 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.

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

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

Cuestiones relacionadas