Tengo dos procesos fooy bar, conectado con una tubería:
$ foo | bar
barsiempre sale 0; Estoy interesado en el código de salida de foo. ¿Hay alguna manera de llegar a eso?
Tengo dos procesos fooy bar, conectado con una tubería:
$ foo | bar
barsiempre sale 0; Estoy interesado en el código de salida de foo. ¿Hay alguna manera de llegar a eso?
Respuestas:
Si está usando bash, puede usar la PIPESTATUSvariable de matriz para obtener el estado de salida de cada elemento de la tubería.
$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0
Si está utilizando zsh, se llama a la matriz pipestatus(¡el caso importa!) Y los índices de la matriz comienzan en uno:
$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0
Para combinarlos dentro de una función de una manera que no pierda los valores:
$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0
Ejecute lo anterior en basho zshy obtendrá los mismos resultados; solo se establecerá uno de retval_bashy retval_zsh. El otro estará en blanco. Esto permitiría que una función terminara return $retval_bash $retval_zsh(¡tenga en cuenta la falta de comillas!).
pipestatusen zsh. Lamentablemente, otros proyectiles no tienen esta característica.
echo "$pipestatus[1]" "$pipestatus[2]".
if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
Hay 3 formas comunes de hacer esto:
La primera forma es establecer la pipefailopción ( ksh, zsho bash). Este es el más simple y lo que hace es básicamente establecer el estado $?de salida en el código de salida del último programa para salir de cero (o cero si todos salieron con éxito).
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Bash también tiene una variable de matriz llamada $PIPESTATUS( $pipestatusin zsh) que contiene el estado de salida de todos los programas en la última canalización.
$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1
Puede usar el tercer ejemplo de comando para obtener el valor específico en la tubería que necesita.
Esta es la más difícil de manejar de las soluciones. Ejecute cada comando por separado y capture el estado
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
ksh, pero de un breve vistazo a su página de manual, no es compatible $PIPESTATUSni nada similar. Sin embargo, admite la pipefailopción.
LOG=$(failed_command | successful_command)
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
el resultado de esta construcción es stdout desde filterstdout de la construcción y el estado de salida desde someprogcomo estado de salida de la construcción.
Esta construcción también funciona con la agrupación de comandos simple en {...}lugar de subcapas (...). Las subcapas tienen algunas implicaciones, entre otras, un costo de rendimiento, que no necesitamos aquí. lea el manual de fine bash para obtener más detalles: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
Desafortunadamente, la gramática bash requiere espacios y puntos y comas para las llaves para que la construcción se vuelva mucho más espaciosa.
Para el resto de este texto, usaré la variante subshell.
Ejemplo someprogy filter:
someprog() {
echo "line1"
echo "line2"
echo "line3"
return 42
}
filter() {
while read line; do
echo "filtered $line"
done
}
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
Salida de ejemplo:
filtered line1
filtered line2
filtered line3
42
Nota: el proceso hijo hereda los descriptores de archivo abiertos del padre. Eso significa someprogque heredará el descriptor de archivo abierto 3 y 4. Si someprogescribe en el descriptor de archivo 3, se convertirá en el estado de salida. El estado de salida real se ignorará porque readsolo se lee una vez.
Si le preocupa que someprogpueda escribir en el descriptor de archivo 3 o 4, entonces es mejor cerrar los descriptores de archivo antes de llamar someprog.
(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
El exec 3>&- 4>&-antes someprogcierra el descriptor de archivo antes de ejecutarlo, someprogpor lo que para someprogesos descriptores de archivo simplemente no existen.
También se puede escribir así: someprog 3>&- 4>&-
Explicación paso a paso de la construcción:
( ( ( ( someprog; #part6
echo $? >&3 #part5
) | filter >&4 #part4
) 3>&1 #part3
) | (read xs; exit $xs) #part2
) 4>&1 #part1
De abajo hacia arriba:
#part3) y la derecha ( #part2). exit $xstambién es el último comando de la tubería y eso significa que la cadena de stdin será el estado de salida de toda la construcción.#part2y, a su vez, será el estado de salida de toda la construcción.#part5y #part6) y a la derecha ( filter >&4). La salida de filterse redirige al descriptor de archivo 4. En #part1el descriptor de archivo 4 se redirige a stdout. Esto significa que la salida de filteres la salida estándar de toda la construcción.#part6se imprime en el descriptor de archivo 3. En #part3el descriptor de archivo 3 se redirige a #part2. Esto significa que el estado de salida de #part6será el estado de salida final para toda la construcción.someproges ejecutado. Se toma el estado de salida #part5. La tubería toma el stdout #part4y lo reenvía filter. La salida de filtera su vez alcanzará stdout como se explica en#part4(read; exit $REPLY)
(exec 3>&- 4>&-; someprog)simplifica a someprog 3>&- 4>&-.
{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
Si bien no es exactamente lo que pediste, podrías usar
#!/bin/bash -o pipefail
para que sus tuberías devuelvan el último retorno distinto de cero.
podría ser un poco menos codificación
Editar: Ejemplo
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
set -o pipefaildentro del script debe ser más robusto, por ejemplo, en caso de que alguien ejecute el script a través de bash foo.sh.
-o pipefailno está en POSIX.
#!/bin/bash -o pipefail. El error es:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
#!líneas más allá de la primera, y por lo que este se convierte /bin/bash -o pipefail /tmp/ff, en lugar de lo necesario /bin/bash -o pipefail /tmp/ff- getoptel análisis sintáctico (o similar) con el optarg, que es el siguiente elemento ARGV, como el argumento a -o, entonces falla. Si tuviera que hacer una envoltura (digamos, bash-pfeso acaba de hacer exec /bin/bash -o pipefail "$@", y poner eso en la #!línea, eso funcionaría. Ver también: en.wikipedia.org/wiki/Shebang_%28Unix%29
Lo que hago cuando sea posible es alimentar el código de salida desde foodentro bar. Por ejemplo, si sé que foonunca produce una línea con solo dígitos, entonces puedo agregar el código de salida:
{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'
O si sé que la salida de foonunca contiene una línea con solo .:
{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'
Esto siempre se puede hacer si hay alguna forma de llegar bara trabajar en todos, excepto en la última línea, y pasar la última línea como su código de salida.
Si se bartrata de una tubería compleja cuya salida no necesita, puede omitir parte de ella imprimiendo el código de salida en un descriptor de archivo diferente.
exit_codes=$({ { foo; echo foo:"$?" >&3; } |
{ bar >/dev/null; echo bar:"$?" >&3; }
} 3>&1)
Después de esto $exit_codeses generalmente foo:X bar:Y, pero podría ser bar:Y foo:Xsi se barcierra antes de leer toda su entrada o si tiene mala suerte. Creo que las escrituras en las tuberías de hasta 512 bytes son atómicas en todos los sistemas Unix, por lo que los foo:$?y bar:$?las piezas no se pueden mezclar, siempre y cuando las cuerdas están bajo la etiqueta de 507 bytes.
Si necesita capturar la salida bar, se hace difícil. Puede combinar las técnicas anteriores organizando la salida de barnunca para que contenga una línea que parezca una indicación de código de salida, pero se vuelve complicada.
output=$(echo;
{ { foo; echo foo:"$?" >&3; } |
{ bar | sed 's/^/^/'; echo bar:"$?" >&3; }
} 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')
Y, por supuesto, existe la opción simple de usar un archivo temporal para almacenar el estado. Simple, pero no tan simple en producción:
/tmpes el único lugar donde un script seguramente podrá escribir archivos. Uso mktemp, que no es POSIX pero está disponible en todas las unidades serias hoy en día.foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
A partir de la tubería:
foo | bar | baz
Aquí hay una solución general que usa solo shell POSIX y no archivos temporales:
exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
(bar || echo "1:$?" >&3) |
(baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-
$error_statuses contiene los códigos de estado de cualquier proceso fallido, en orden aleatorio, con índices para indicar qué comando emitió cada estado.
# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2
# test if all commands succeeded:
test -z "$error_statuses"
# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null
Tenga en cuenta las citas $error_statusesen mis pruebas; sin ellos grepno se puede diferenciar porque las nuevas líneas se convierten en espacios forzados.
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 command1 en su stdout, pero ese 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 volveremos a colocar la salida de printf en el descriptor de archivo 3 en el 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 se "sale" de la sustitución del comando, efectivamente sigue yendo al descriptor de archivo general 1 del script.
( 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 uso 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.
-bash: 3: Bad file descriptor.
Si tiene instalado el paquete moreutils , puede usar la utilidad mispipe que hace exactamente lo que solicitó.
La solución anterior de lesmana también se puede hacer sin la sobrecarga de iniciar subprocesos anidados utilizando en su { .. }lugar (recordando que esta forma de comandos agrupados siempre tiene que terminar con punto y coma). Algo como esto:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
He comprobado esta construcción con la versión de guión 0.5.5 y las versiones de bash 3.2.25 y 4.2.42, por lo que incluso si algunos shells no admiten la { .. }agrupación, sigue siendo compatible con POSIX.
set -o pipefailin ksh o cualquier número de waitcomandos rociados en cualquiera de ellos. Creo que, en parte, al menos, puede ser un problema de análisis de ksh, ya que si me limito a usar subshells, entonces funciona bien, pero incluso con un ifpara elegir la variante de subshell para ksh pero dejar los comandos compuestos para otros, falla .
Esto es portátil, es decir, funciona con cualquier shell compatible con POSIX, no requiere que el directorio actual sea editable y permite que se ejecuten simultáneamente varios scripts que usan el mismo truco.
(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))
Editar: aquí hay una versión más fuerte después de los comentarios de Gilles:
(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))
Edit2: y aquí hay una variante ligeramente más ligera después del comentario dudoso de Jim:
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Estoy de acuerdo en que es más fácil con Bash, pero en algunos contextos, vale la pena saber cómo evitarlo.
Siguiente se entiende como un complemento a la respuesta de @Patrik, en caso de que no pueda utilizar una de las soluciones comunes.
Esta respuesta supone lo siguiente:
$PIPESTATUSniset -o pipefailSuposiciones adicionales. Puede deshacerse de todo, pero esto cambia demasiado la receta, por lo que no se trata aquí:
- Todo lo que quiere saber es que todos los comandos en PIPE tienen el código de salida 0.
- No necesita información adicional sobre la banda lateral.
- Su shell espera a que regresen todos los comandos de tubería.
Antes: foo | bar | bazsin embargo, esto solo devuelve el código de salida del último comando ( baz)
Se busca: $?no debe ser 0(verdadero), si alguno de los comandos en la tubería falló
Después:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
# $? now is 0 only if all commands had exit code 0
Explicado:
mktemp. Esto generalmente crea inmediatamente un archivo en/tmpwaitnecesita para ksh, porque de lo kshcontrario no espera a que finalicen todos los comandos de tubería. Sin embargo, tenga en cuenta que hay efectos secundarios no deseados si hay algunas tareas en segundo plano, por lo que lo comenté de forma predeterminada. Si la espera no duele, puedes comentarla.readregresa false, entonces trueindica un errorEsto se puede usar como reemplazo de un complemento para un solo comando y solo necesita lo siguiente:
/proc/fd/NLoco:
Este script tiene un error en caso de que se /tmpquede sin espacio. Si también necesita protección contra este caso artificial, puede hacerlo de la siguiente manera, sin embargo, esto tiene la desventaja de que el número de 0in 000depende del número de comandos en la tubería, por lo que es un poco más complicado:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
Notas de portabilidad:
kshy los shells similares que solo esperan el último comando de tubería necesitan el waitcomentario no comentado
El último ejemplo se usa en printf "%1s" "$?"lugar de echo -n "$?"porque es más portátil. No todas las plataformas interpretan -ncorrectamente.
printf "$?"lo haría también, sin embargo, printf "%1s"detecta algunos casos de esquina en caso de que ejecute el script en una plataforma realmente rota. (Lea: si programa en paranoia_mode=extreme).
FD 8 y FD 9 pueden ser superiores en plataformas que admiten múltiples dígitos. AFAIR un shell POSIX conforme solo necesita admitir dígitos individuales.
Se puso a prueba con Debian 8.2 sh, bash, ksh, ash, sashe inclusocsh
Con un poco de precaución, esto debería funcionar:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
El siguiente bloque 'if' se ejecutará solo si 'command' se realizó correctamente:
if command; then
# ...
fi
Hablando específicamente, puede ejecutar algo como esto:
haconf_out=/path/to/some/temporary/file
if haconf -makerw > "$haconf_out" 2>&1; then
grep -iq "Cluster already writable" "$haconf_out"
# ...
fi
Que ejecutará haconf -makerwy almacenará su stdout y stderr en "$ haconf_out". Si el valor devuelto desde haconfes verdadero, entonces el bloque 'if' se ejecutará y grepleerá "$ haconf_out", intentando compararlo con "Cluster ya escribible".
Observe que las tuberías se limpian automáticamente; con la redirección, deberá tener cuidado de eliminar "$ haconf_out" cuando haya terminado.
No es tan elegante como pipefail, pero es una alternativa legítima si esta funcionalidad no está al alcance.
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"
exec 8>&- 9>&-
{
{
{
{ #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8 # use exactly 1x prior to #END
#END
} 2>&1 | ${TEE} 1>&9
} 8>&1
} | exit $(read; printf "${REPLY}")
} 9>&1
exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
(Con bash al menos) combinado con set -euno puede usar subshell para emular explícitamente pipefail y salir en error de tubería
set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program
Entonces, si foofalla por alguna razón, el resto del programa no se ejecutará y el script se cerrará con el código de error correspondiente. (Esto supone que fooimprime su propio error, que es suficiente para comprender el motivo del fallo)
EDITAR : Esta respuesta es incorrecta, pero interesante, así que la dejaré para referencia futura.
!a el comando invierte el código de retorno.
http://tldp.org/LDP/abs/html/exit-status.html
# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command # bash: bogus_command: command not found
echo $? # 127
! ls | bogus_command # bash: bogus_command: command not found
echo $? # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
ls, no invertir el código de salida debogus_command