Tengo dos procesos foo
y bar
, conectado con una tubería:
$ foo | bar
bar
siempre sale 0; Estoy interesado en el código de salida de foo
. ¿Hay alguna manera de llegar a eso?
Tengo dos procesos foo
y bar
, conectado con una tubería:
$ foo | bar
bar
siempre 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 PIPESTATUS
variable 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 bash
o zsh
y obtendrá los mismos resultados; solo se establecerá uno de retval_bash
y 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!).
pipestatus
en 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 pipefail
opción ( ksh
, zsh
o 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
( $pipestatus
in 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 $PIPESTATUS
ni nada similar. Sin embargo, admite la pipefail
opció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 someprog
y 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 filter
stdout de la construcción y el estado de salida desde someprog
como 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 someprog
y 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 someprog
que heredará el descriptor de archivo abierto 3 y 4. Si someprog
escribe en el descriptor de archivo 3, se convertirá en el estado de salida. El estado de salida real se ignorará porque read
solo se lee una vez.
Si le preocupa que someprog
pueda 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 someprog
cierra el descriptor de archivo antes de ejecutarlo, someprog
por lo que para someprog
esos 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 $xs
tambié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.#part2
y, a su vez, será el estado de salida de toda la construcción.#part5
y #part6
) y a la derecha ( filter >&4
). La salida de filter
se redirige al descriptor de archivo 4. En #part1
el descriptor de archivo 4 se redirige a stdout. Esto significa que la salida de filter
es la salida estándar de toda la construcción.#part6
se imprime en el descriptor de archivo 3. En #part3
el descriptor de archivo 3 se redirige a #part2
. Esto significa que el estado de salida de #part6
será el estado de salida final para toda la construcción.someprog
es ejecutado. Se toma el estado de salida #part5
. La tubería toma el stdout #part4
y lo reenvía filter
. La salida de filter
a 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 pipefail
dentro 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 pipefail
no 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
- getopt
el 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-pf
eso 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 foo
dentro bar
. Por ejemplo, si sé que foo
nunca 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 foo
nunca contiene una línea con solo .
:
{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'
Esto siempre se puede hacer si hay alguna forma de llegar bar
a trabajar en todos, excepto en la última línea, y pasar la última línea como su código de salida.
Si se bar
trata 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_codes
es generalmente foo:X bar:Y
, pero podría ser bar:Y foo:X
si se bar
cierra 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 bar
nunca 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:
/tmp
es 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_statuses
en mis pruebas; sin ellos grep
no 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>&1
lo 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>&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 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>&1
heredará 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 pipefail
in ksh o cualquier número de wait
comandos 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 if
para 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:
$PIPESTATUS
niset -o pipefail
Suposiciones 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 | baz
sin 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/tmp
wait
necesita para ksh
, porque de lo ksh
contrario 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.read
regresa false
, entonces true
indica un errorEsto se puede usar como reemplazo de un complemento para un solo comando y solo necesita lo siguiente:
/proc/fd/N
Loco:
Este script tiene un error en caso de que se /tmp
quede 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 0
in 000
depende 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:
ksh
y los shells similares que solo esperan el último comando de tubería necesitan el wait
comentario 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 -n
correctamente.
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
, sash
e 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 -makerw
y almacenará su stdout y stderr en "$ haconf_out". Si el valor devuelto desde haconf
es verdadero, entonces el bloque 'if' se ejecutará y grep
leerá "$ 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 -e
uno 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 foo
falla 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 foo
imprime 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