La salida de sustitución del proceso está fuera de orden

16

los

echo one; echo two > >(cat); echo three; 

El comando da una salida inesperada.

Leí esto: ¿Cómo se implementa la sustitución de procesos en bash? y muchos otros artículos sobre la sustitución de procesos en Internet, pero no entiendo por qué se comporta de esta manera.

Rendimiento esperado:

one
two
three

Salida real:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Además, estos dos comandos deberían ser equivalentes desde mi punto de vista, pero no lo hacen:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

¿Por qué creo que deberían ser lo mismo? Porque ambos conectan la seqsalida a la catentrada a través de la tubería anónima: Wikipedia, Sustitución de procesos .

Pregunta: ¿Por qué se comporta de esta manera? ¿Dónde está mi error? Se desea la respuesta integral (con una explicación de cómo bashfunciona bajo el capó).

MiniMax
fuente
2
Incluso si no es tan claro a primera vista, en realidad es un duplicado de bash wait para el proceso en la sustitución del proceso, incluso si el comando no es válido
Stéphane Chazelas
2
En realidad, sería mejor si esa otra pregunta se marcara como un duplicado de esta, ya que esta es más importante. Por eso copié mi respuesta allí.
Stéphane Chazelas

Respuestas:

21

Sí, al bashigual que en ksh(de dónde proviene la característica), no se esperan los procesos dentro de la sustitución del proceso (antes de ejecutar el siguiente comando en el script).

para <(...)uno, eso generalmente está bien como en:

cmd1 <(cmd2)

el caparazón estará esperando cmd1y cmd1típicamente estará esperando en cmd2virtud de leer hasta el final del archivo en la tubería que se sustituye, y ese final del archivo generalmente ocurre cuando cmd2muere. Esa es la misma razón varios proyectiles (no bash) no se molestan en espera de cmd2en cmd2 | cmd1.

Para cmd1 >(cmd2), sin embargo, eso no es generalmente el caso, ya que es más cmd2que normalmente espera a que cmd1no lo hará por lo general después de la salida.

Eso se solucionó en las zshesperas cmd2allí (pero no si lo escribe como cmd1 > >(cmd2)y cmd1no está integrado, use {cmd1} > >(cmd2)en su lugar como se documenta ).

kshno espera de forma predeterminada, pero le permite esperar con el waitincorporado (también hace que el pid esté disponible $!, aunque eso no ayuda si lo hace cmd1 >(cmd2) >(cmd3))

rc(con la cmd1 >{cmd2}sintaxis), igual que, kshexcepto que puede obtener los pids de todos los procesos en segundo plano $apids.

es(también con cmd1 >{cmd2}) espera cmd2como en zsh, y también espera redirecciones cmd2en <{cmd2}proceso.

bashhace que el pid de cmd2(o más exactamente de la subshell ya que se ejecuta cmd2en un proceso secundario de esa subshell aunque sea el último comando allí) esté disponible $!, pero no te deja esperar.

Si tiene que usarlo bash, puede solucionar el problema utilizando un comando que esperará ambos comandos con:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Eso hace que tanto cmd1y cmd2tener su fd 3 abierta a una tubería. catesperará al final de su archivo en el otro extremo, por lo general sólo se salga cuando ambos cmd1y cmd2están muertos. Y el shell esperará ese catcomando. Podrías verlo como una red para detectar la finalización de todos los procesos en segundo plano (puedes usarlo para otras cosas que se inician en segundo plano, como con &, coprocs o incluso comandos que se ejecutan en segundo plano, siempre que no cierren todos sus descriptores de archivos como suelen hacer los demonios )

Tenga en cuenta que gracias al proceso de subshell desperdiciado mencionado anteriormente, funciona incluso si cmd2cierra su fd 3 (los comandos generalmente no hacen eso, pero a algunos les gusta sudoo lo sshhacen). Las versiones futuras de basheventualmente pueden hacer la optimización allí como en otros shells. Entonces necesitarías algo como:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Para asegurarse de que todavía hay un proceso de shell adicional con ese fd 3 abierto esperando ese sudocomando.

Tenga en cuenta que catno leerá nada (ya que los procesos no escriben en su fd 3). Solo está ahí para la sincronización. Solo hará una read()llamada al sistema que regresará sin nada al final.

En realidad, puede evitar la ejecución catutilizando una sustitución de comando para realizar la sincronización de la tubería:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Esta vez, es el caparazón en lugar de lo catque está leyendo desde la tubería cuyo otro extremo está abierto en fd 3 de cmd1y cmd2. Estamos utilizando una asignación variable para que el estado de salida de cmd1esté disponible en $?.

O podría hacer la sustitución del proceso a mano, y luego incluso podría usar el sistema, shya que eso se convertiría en la sintaxis estándar de la shell:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

aunque tenga en cuenta, como se señaló anteriormente, que no todas las shimplementaciones esperarían cmd1después de que cmd2haya terminado (aunque eso es mejor que al revés). Ese tiempo, $?contiene el estado de salida de cmd2; sin embargo, bashy zshhacer que cmd1el estado de salida esté disponible en ${PIPESTATUS[0]}y $pipestatus[1]respectivamente (consulte también la pipefailopción en algunos shells para $?poder informar el fallo de los componentes de la tubería que no sean el último)

Tenga en cuenta que yashtiene problemas similares con su función de redirección de procesos . cmd1 >(cmd2)Sería escrito cmd1 /dev/fd/3 3>(cmd2)allí. Pero cmd2no se espera y tampoco se puede usar waitpara esperarlo, y su pid tampoco está disponible en la $!variable. Usarías las mismas soluciones alternativas que para bash.

Stéphane Chazelas
fuente
En primer lugar, lo intenté echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, luego lo simplifiqué echo one; echo two > >(cat) | cat; echo three;y también muestra los valores en el orden correcto. ¿Todas estas manipulaciones de descriptores 3>&1 >&4 4>&-son necesarias? Además, no entiendo esto >&4 4>&: somos redirigidos stdoutal cuarto fd, luego cerramos el cuarto fd y luego lo usamos nuevamente 4>&1. ¿Por qué lo necesitaba y cómo funciona? Puede ser, ¿debería crear una nueva pregunta sobre este tema?
MiniMax
1
@MiniMax, pero allí, estás afectando el stdout de , cmd1y cmd2el punto con el pequeño baile con el descriptor de archivo es restaurar los originales y usar solo la tubería adicional para la espera en lugar de también canalizar la salida de los comandos.
Stéphane Chazelas
@MiniMax Me tomó un tiempo comprender que antes no tenía las tuberías a un nivel tan bajo. El extremo derecho 4>&1crea un descriptor de archivo (fd) 4 para la lista de comandos de llaves externas, y lo hace igual a la salida estándar de llaves externas. Las llaves internas tienen stdin / stdout / stderr configuradas automáticamente para conectarse a las llaves externas. Sin embargo, 3>&1hace que fd 3 se conecte al stdin de los brackets externos. >&4hace que el stdout de los brackets internos se conecte con los brackets externos fd 4 (El que creamos antes). 4>&-cierra fd 4 de las llaves internas (dado que la salida estándar de las llaves internas ya está conectada a las llaves externas fd 4).
Nicholas Pipitone
@MiniMax La parte confusa era la parte de derecha a izquierda, 4>&1se ejecuta primero, antes que las otras redirecciones, por lo que no "vuelve a utilizar 4>&1". En general, las llaves internas están enviando datos a su stdout, que se sobrescribió con cualquier fd 4 que se le proporcionó. El fd 4 que se le dio a los brackets internos es el fd 4 de los brackets externos, que es igual al stdout original de los brackets externos.
Nicholas Pipitone
Bash hace que parezca que 4>5significa "4 va a 5", pero realmente "fd 4 se sobrescribe con fd 5". Y antes de la ejecución, fd 0/1/2 se conectan automáticamente (junto con cualquier fd del shell externo), y puede sobrescribirlos como lo desee. Esa es al menos mi interpretación de la documentación de bash. Si entendiste algo más de esto , lmk.
Nicholas Pipitone
4

Puede canalizar el segundo comando a otro cat, que esperará hasta que se cierre su canal de entrada. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Corto y sencillo.

==========

Tan simple como parece, están sucediendo muchas cosas detrás de escena. Puede ignorar el resto de la respuesta si no está interesado en cómo funciona esto.

Cuando lo haya hecho echo two > >(cat); echo three, >(cat)el shell interactivo lo bifurca y se ejecuta independientemente de echo two. Por lo tanto, echo twotermina, y luego echo threese ejecuta, pero antes de que >(cat)termine. Cuando bashobtiene datos de >(cat)cuando no los esperaba (un par de milisegundos más tarde), le da esa situación similar a la de un aviso en el que debe presionar la línea nueva para volver al terminal (lo mismo que si otro usuario lo mesgeditara).

Sin embargo, dado echo two > >(cat) | cat; echo three, se generan dos subcapas (según la documentación del |símbolo).

Una subshell llamada A es para echo two > >(cat), y una subshell llamada B es para cat. A se conecta automáticamente a B (la salida estándar de A es la entrada estándar de B). Entonces, echo twoy >(cat)comienza a ejecutar. >(cat)La salida estándar se establece en la salida estándar de A, que es igual a la entrada estándar de B. Después de que echo twotermina, A sale, cerrando su stdout. Sin embargo, >(cat)todavía mantiene la referencia al stdin de B. El catstdin del segundo contiene el stdin de B, y eso catno saldrá hasta que vea un EOF. Un EOF solo se da cuando ya nadie tiene el archivo abierto en modo de escritura, por lo que >(cat)stdout está bloqueando el segundo cat. B sigue esperando ese segundo cat. Desde que echo twosalió, >(cat)finalmente obtiene un EOF, así>(cat)descarga su búfer y sale. Ya nadie tiene el catstdin de B / segundo , por lo que el segundo catlee un EOF (B no está leyendo su stdin en absoluto, no le importa). Este EOF hace que el segundo catvacíe su búfer, cierre su salida estándar y salga, y luego B sale porque catsalió y B estaba esperando cat.

Una advertencia de esto es que bash también genera una subshell para >(cat)! Debido a esto, verás que

echo two > >(sleep 5) | cat; echo three

aún esperará 5 segundos antes de ejecutar echo three, aunque sleep 5no tenga el stdin de B. Esto se debe a que una subshell oculta C engendrada >(sleep 5)está esperando sleepy C mantiene el stdin de B. Puedes ver como

echo two > >(exec sleep 5) | cat; echo three

Sin embargo, no esperará, ya sleepque no está sosteniendo el stdin de B, y no hay una subshell fantasma C que esté sosteniendo el stdin de B (el ejecutivo obligará a dormir a reemplazar a C, en lugar de bifurcar y hacer que C espere sleep). Independientemente de esta advertencia,

echo two > >(exec cat) | cat; echo three

seguirá ejecutando correctamente las funciones en orden, como se describió anteriormente.

Nicholas Pipitone
fuente
Como se señaló en la conversión con @MiniMax en los comentarios a mi respuesta, eso tiene el inconveniente de afectar la salida estándar del comando y significa que la salida debe leerse y escribirse un tiempo extra.
Stéphane Chazelas
La explicación no es precisa. ANo está esperando a los catengendrados >(cat). Como mencioné en mi respuesta, la razón por la que echo two > >(sleep 5 &>/dev/null) | cat; echo threesale threedespués de 5 segundos es porque las versiones actuales de bashdesperdicio de un proceso de shell adicional en >(sleep 5)espera sleepy ese proceso todavía tiene stdout yendo al pipeque impide que el segundo cattermine. Si lo reemplaza echo two > >(exec sleep 5 &>/dev/null) | cat; echo threepara eliminar ese proceso adicional, encontrará que regresa de inmediato.
Stéphane Chazelas
¿Hace una subshell anidada? He estado tratando de analizar la implementación de bash para resolverlo, estoy bastante seguro de que, echo two > >(sleep 5 &>/dev/null)como mínimo, tiene su propia subshell. ¿Es un detalle de implementación no documentado que hace sleep 5que también obtenga su propia subshell? Si está documentado, sería una forma legítima de hacerlo con menos caracteres (a menos que haya un bucle cerrado, no creo que alguien note problemas de rendimiento con un subshell o un gato) `. Si no está documentado, entonces rip, un buen truco, no funcionará en versiones futuras.
Nicholas Pipitone
$(...), de <(...)hecho, implican una subshell, pero ksh93 o zsh ejecutarían el último comando en esa subshell en el mismo proceso, y no bashes por eso que todavía hay otro proceso que mantiene la tubería abierta mientras sleepse ejecuta y no mantiene la tubería abierta. Las versiones futuras de bashpueden implementar una optimización similar.
Stéphane Chazelas
1
@ StéphaneChazelas Actualicé mi respuesta y creo que la explicación actual de la versión más corta es correcta, pero parece conocer los detalles de implementación de los shells para que pueda verificar. Sin embargo, creo que esta solución debería usarse en lugar de la descripción del archivo dance, ya que incluso debajo execfunciona como se esperaba.
Nicholas Pipitone