tuberias script salida redireccionamiento pipes manejo guardar filtros ejemplos comandos comando cadenas archivo bash shell error-handling pipe

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



redireccionamiento linux ejemplos (15)

A veces puede ser más sencillo y claro usar un comando externo, en lugar de profundizar en los detalles de bash. pipeline , desde el execline lenguaje de scripting de proceso mínimo, sale con el código de retorno del segundo comando *, al igual que lo hace sh pipeline, pero a diferencia de sh , permite invertir la dirección de la tubería, para que podamos capturar el código de retorno de el proceso del productor (el siguiente se encuentra en la línea de comando 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 de bash nativas que la sustitución del proceso de bash utilizada en la respuesta #43972501 .

* En realidad, la 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 devuelve.

Quiero ejecutar un comando de ejecución prolongada 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 tee de tee y no de comando. ¿Como puedo resolver esto?

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


Al combinar PIPESTATUS[0] y el resultado de ejecutar el comando 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 quise aportar 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.

# 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), luego, una vez hecho esto, printf ejecutará e imprimirá el código de salida de icommand1 en su stdout, pero esa 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 archivos 3 en lugar de 1, que es lo que lee la canalización). Luego redirigimos la salida de command2 al descriptor de archivo 4, para que también se quede fuera del descriptor de archivo 1, ya que queremos que el descriptor de archivo 1 esté libre un poco más tarde, ya que traeremos la salida de printf en el descriptor de archivo 3 de vuelta al descriptor de archivo 1 - porque eso es lo que el comando de sustitución (las comillas invertidas) capturará y eso es lo que se colocará en la variable.

El bit final de 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 el estándar desde la perspectiva de los comandos internos, pero como la salida de command2 va al descriptor de archivos 4 en lo que respecta a la sustitución de comandos, la sustitución de comandos no lo captura, sin embargo, una vez que obtiene "fuera" de la sustitución de comandos que efectivamente sigue yendo al descriptor de archivos general 1 del script.

(El exec 4>&1 tiene 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 usa la sustitución. Así que esta es la forma portátil más simple de hacerlo.)

Puedes verlo de una manera menos técnica y más lúdica, como si las salidas de los comandos se superponen entre sí: comando1 canaliza a comando2, entonces la salida de printf salta sobre el comando 2 para que comando2 no lo atrape, y luego La salida del comando 2 salta por encima de la sustitución del comando, al igual que printf llega justo a tiempo para ser capturada por la sustitución para que termine en la variable, y la salida del comando2 continúa su camino alegre y se escribe en la salida estándar, igual que en una pipa normal.

Además, como yo lo entiendo, $? aún contendrá el código de retorno del segundo comando en la tubería, ya que las asignaciones de variables, las sustituciones de comando y los comandos compuestos son transparentes al código de retorno del comando dentro de ellos, por lo que el estado de retorno del comando2 debe propagarse, esto , y no tener que definir una función adicional, es por eso que creo que esta podría ser una solución algo mejor que la propuesta por lesmana.

Por las advertencias que menciona Lesmana, es posible que el comando 1 en algún momento termine usando los descriptores de archivos 3 o 4, por lo que para ser más sólido, debería hacer:

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

Los comandos heredan los descriptores de archivos del proceso que los inicia, por lo que la segunda línea completa heredará el descriptor de archivos cuatro, y el comando compuesto seguido de 3>&1 heredará el descriptor de archivos tres. Por lo tanto, el 4>&- se asegura de que el comando compuesto interno no herede el descriptor de archivo cuatro, y el 3>&- no hereda 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 interior 4>&- lado del 3>&- , pero me imagino por qué no limitar su alcance tanto como sea posible.

No estoy seguro de la frecuencia con la que las cosas usan el descriptor de archivos tres y cuatro directamente. Creo que la mayoría de las veces los programas usan syscalls que devuelven descriptores de archivos no usados ​​en el momento, pero a veces las escrituras de código en el descriptor de archivos 3 directamente. adivina (podría imaginar un programa que verifique un descriptor de archivo para ver si está abierto y usarlo si lo está, o comportarse 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.


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

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

utilizado así:

1: get_bad_things debe tener éxito, pero no debe producir salida; Pero queremos ver la salida que produce

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

2: todas las tuberías deben tener éxito

thing | something -q | thingy pipeinfo || return


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.


Esta solución funciona sin usar funciones 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 que 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 $?

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


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 .


Hay una matriz que le da el estado de salida de cada comando en una canalización.

$ 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


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

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

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

set -o pipefail ...

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


La forma más sencilla de hacer esto en un simple bash es usar la sustitución de procesos en lugar de una tubería. Existen varias diferencias, pero probablemente no importen 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 elimine todos los procesos de una canalización, no solo el principal.
  • La opción pipefail y la variable PIPESTATUS son irrelevantes para procesar la sustitución.
  • Posiblemente más

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

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

Si desea voltear cuál es el proceso "principal", solo debe voltear 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. El proceso nunca puede dejar de ejecutarse, a menos que sea sensible al cierre de la tubería. En particular, puede seguir escribiendo cosas a su stdout, lo que puede ser confuso.


PIPESTATUS [@] debe copiarse en una matriz inmediatamente después de que se devuelva el comando de tubería. Cualquier lectura de PIPESTATUS [@] borrará el contenido. Cópielo en otra matriz si planea verificar el estado de todos los comandos de tuberías. PS es el mismo valor que el último elemento de "$ {PIPESTATUS [@]}", y su lectura 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 sub-shell. Para una solución a ese problema,
¿Ver bash pipestatus en el comando Backticked?


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

Por favor, tenga en cuenta que el primer gato también falla, porque su stdout 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 luego pueda volcar eso también en un archivo de registro si ocurre un error, o simplemente eliminarlo si no hay ningún error (como la salida de dd).


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


usar el set -o pipefail es útil

pipefail: el valor de retorno de una tubería es el estado del último comando para salir con un estado distinto de cero, o cero si no se sale de un comando con un estado distinto de cero


(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 @Chaoran señaló, puedes llamar a ${PIPESTATUS[0]} . Es importante sin embargo que todo se ponga entre paréntesis.