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

421

Quiero ejecutar un comando de larga duración en Bash, y tanto la captura de su estado de salida, y el primer golpe su salida.

Entonces hago esto:

command | tee out.txt
ST=$?

El problema es que la variable ST captura el estado de salida teey no del comando. ¿Como 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í.

flybywire
fuente
1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Coincidencia: error encontrado" || echo -e "No match - all good" Esto probará todos los valores de la matriz a la vez y dará un mensaje de error si alguno de los valores de tubería devueltos no son cero. Esta es una solución generalizada bastante robusta para detectar errores en una situación canalizada.
Brian S. Wilson
unix.stackexchange.com/questions/14270/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

519

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

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

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

set -o pipefail
...

La primera opción no funciona zshdebido a una sintaxis un poco diferente.

codar
fuente
21
Hay una buena explicación con ejemplos de PIPESTATUS AND Pipefail aquí: unix.stackexchange.com/a/73180/7453 .
slm
18
Nota: $ PIPESTATUS [0] mantiene el estado de salida del primer comando en la tubería, $ PIPESTATUS [1] el estado de salida del segundo comando, y así sucesivamente.
simpleuser
18
Por supuesto, debemos 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 integrada usando alguna otra "sh" variante, esto no funcionaría.
Asfand Qazi
44
Para aquellos preocupados por la expansión de variables sin comillas: el estado de salida siempre es un entero de 8 bits sin signo en Bash , por lo tanto, no es necesario citarlo. En general, esto también se cumple en Unix, donde el estado de salida se define explícitamente como 8 bits , y se supone que no está firmado incluso por POSIX, por ejemplo, al definir su negación lógica .
Palec
3
También puedes usar exit ${PIPESTATUS[0]}.
Chaoran
142

usar bash set -o pipefailes ú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 ningún comando salió con un estado distinto de cero

Felipe Alvarez
fuente
23
En caso de que no desee modificar la configuración de falla de tubería de todo el script, puede configurar la opción solo localmente:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan
77
@Jaan Esto ejecutaría una subshell. Si desea evitar eso, puede hacer set -o pipefaily luego hacer el comando, e inmediatamente después set +o pipefaildeshabilitar la opción.
Linus Arver
2
Nota: el póster de la pregunta no quiere un "código de salida general" de la tubería, quiere el código de retorno de 'comando'. Con -o pipefailél sabría si la tubería falla, pero si fallan tanto 'comando' como 'tee', recibiría el código de salida de 'tee'.
t0r0X
@LinusArver, ¿no borraría eso el código de salida ya que es un comando que tiene éxito?
carlin.scott
127

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 $?
EFraim
fuente
20
Esta es la única respuesta en esta pregunta que también funciona para el shell sh Unix simple . ¡Gracias!
JamesThomasMoon1979
3
@DaveKennedy: tonto como en "obvio, no requiere un conocimiento complejo de la sintaxis bash"
EFraim
10
Si bien las respuestas de bash son más elegantes cuando tiene la ventaja de las capacidades adicionales de bash, esta es la solución más multiplataforma. También es algo en lo que vale la pena pensar en general, ya que cada vez que está ejecutando un comando de larga duración, una canalización de nombres suele ser la forma más flexible. Vale la pena señalar que algunos sistemas no tienen mkfifoy pueden requerirlo mknod -psi no recuerdo mal.
Haravikk
3
A veces, en el desbordamiento de la pila, hay respuestas que votarías cientos de veces para que la gente dejara de hacer otras cosas que no tienen sentido, esta es una de ellas. Gracias Señor.
Dan Chase
1
En caso de que alguien tiene un problema con mkfifoo mknod -p: en mi caso de comando apropiado para crear el archivo de la tubería era mknod FILE_NAME p.
Karol Gil
36

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
Stefano Borini
fuente
26

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

echo $?

Consulte mi respuesta para la misma pregunta en unix.stackexchange.com para obtener una explicación detallada y una alternativa sin subcapas y algunas advertencias.

lesmana
fuente
20

Al combinar PIPESTATUS[0]y el resultado de ejecutar el exitcomando 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

par
fuente
44
Gracias, esto me permitió usar la construcción: VALUE=$(might_fail | piping)que no establecerá PIPESTATUS en el shell maestro pero establecerá su nivel de error. Al usar: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})obtengo lo que quería.
vaab
@vaab, esa sintaxis se ve muy bien, pero estoy confundido sobre lo que significa "tubería" en su contexto. ¿Es ahí donde uno haría 'tee' o cualquier procesamiento en la salida de might_fail? ty!
AnneTheAgile
1
@AnneTheAgile 'piping' en mi ejemplo representa comandos desde los cuales no desea ver el errlvl. Por ejemplo: una o cualquier combinación canalizada de 'tee', 'grep', 'sed', ... No es tan raro que estos comandos de tubería estén destinados a formatear o extraer información de una salida más grande o salida de registro de la principal comando: está 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. ¿Es esto más claro?
vaab
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}en caso de que no sea tee sino filtrado grep
user1742529
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 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 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 traeremos la salida de printf en el descriptor de archivo 3 nuevamente al 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 "sale" de la sustitución del comando, efectivamente sigue yendo al descriptor de archivo general del script 1.

( 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 utilizo 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.

mtraceur
fuente
Buena explicación!
selurvedu
6

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

Bryan Larsen
fuente
5
(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, puedes llamar ${PIPESTATUS[0]}. Sin embargo, es importante que todo se ponga entre paréntesis.

jakob-r
fuente
4

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 esté el shell /bin/sh.

Anthony Scemama
fuente
3

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

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,
vea bash pipestatus en el comando backticked?

maxdev137
fuente
3

La forma más simple de hacer esto en bash simple es usar la sustitución de procesos en lugar de una tubería. Existen varias diferencias, pero probablemente no importen demasiado para su caso de uso:

  • Al ejecutar una tubería, bash espera hasta que se completen todos los procesos.
  • Enviar Ctrl-C a bash hace que elimine todos los procesos de una tubería, no solo el principal.
  • La pipefailopción y la PIPESTATUSvariable son irrelevantes para la sustitución de procesos.
  • Posiblemente más

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

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

Si desea voltear cuál es el proceso "principal", simplemente voltee los comandos y la dirección de la sustitución 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 en su stdout, lo que puede ser confuso.

clacke
fuente
1

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 catreemplazado 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 eso también en un archivo de registro si se produce un error, o simplemente eliminarlo si no hay error (como la salida de dd).

Coroos
fuente
1

Base en la respuesta de @ brian-s-wilson; esta función de ayuda 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 resultados; pero queremos ver la salida que produce

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

2: toda la tubería debe tener éxito

thing | something -q | thingy
pipeinfo || return
Sam Liddicott
fuente
1

A veces puede ser más simple y claro usar un comando externo, en lugar de profundizar en los detalles de bash. pipeline , desde el execline del lenguaje de scripting de proceso mínimo , sale con el código de retorno del segundo comando *, al igual que lo hace un shpipeline, pero a diferencia sh, permite invertir la dirección de la tubería, para que podamos capturar el código de retorno del productor proceso (el siguiente está todo en la shlínea de comando, pero con execlineinstalado):

$ # 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 pipelinetiene 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 pipelineno sale en absoluto a menos que haya un error. Se ejecuta en el segundo comando, por lo que es el segundo comando el que hace la devolución.

clacke
fuente