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ó).
bash
process-substitution
MiniMax
fuente
fuente

Respuestas:
Sí, al
bashigual que enksh(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:el caparazón estará esperando
cmd1ycmd1típicamente estará esperando encmd2virtud de leer hasta el final del archivo en la tubería que se sustituye, y ese final del archivo generalmente ocurre cuandocmd2muere. Esa es la misma razón varios proyectiles (nobash) no se molestan en espera decmd2encmd2 | cmd1.Para
cmd1 >(cmd2), sin embargo, eso no es generalmente el caso, ya que es máscmd2que normalmente espera a quecmd1no lo hará por lo general después de la salida.Eso se solucionó en las
zshesperascmd2allí (pero no si lo escribe comocmd1 > >(cmd2)ycmd1no está integrado, use{cmd1} > >(cmd2)en su lugar como se documenta ).kshno espera de forma predeterminada, pero le permite esperar con elwaitincorporado (también hace que el pid esté disponible$!, aunque eso no ayuda si lo hacecmd1 >(cmd2) >(cmd3))rc(con lacmd1 >{cmd2}sintaxis), igual que,kshexcepto que puede obtener los pids de todos los procesos en segundo plano$apids.es(también concmd1 >{cmd2}) esperacmd2como enzsh, y también espera redireccionescmd2en<{cmd2}proceso.bashhace que el pid decmd2(o más exactamente de la subshell ya que se ejecutacmd2en 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:Eso hace que tanto
cmd1ycmd2tener 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 amboscmd1ycmd2están muertos. Y el shell esperará esecatcomando. 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 gustasudoo losshhacen). Las versiones futuras debasheventualmente pueden hacer la optimización allí como en otros shells. Entonces necesitarías algo como: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á unaread()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: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 decmd1ycmd2. Estamos utilizando una asignación variable para que el estado de salida decmd1esté 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:aunque tenga en cuenta, como se señaló anteriormente, que no todas las
shimplementaciones esperaríancmd1después de quecmd2haya terminado (aunque eso es mejor que al revés). Ese tiempo,$?contiene el estado de salida decmd2; sin embargo,bashyzshhacer quecmd1el estado de salida esté disponible en${PIPESTATUS[0]}y$pipestatus[1]respectivamente (consulte también lapipefailopció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 escritocmd1 /dev/fd/3 3>(cmd2)allí. Perocmd2no se espera y tampoco se puede usarwaitpara esperarlo, y su pid tampoco está disponible en la$!variable. Usarías las mismas soluciones alternativas que parabash.fuente
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 descriptores3>&1 >&4 4>&-son necesarias? Además, no entiendo esto>&4 4>&: somos redirigidosstdoutal cuarto fd, luego cerramos el cuarto fd y luego lo usamos nuevamente4>&1. ¿Por qué lo necesitaba y cómo funciona? Puede ser, ¿debería crear una nueva pregunta sobre este tema?cmd1ycmd2el 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.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).4>&1se ejecuta primero, antes que las otras redirecciones, por lo que no "vuelve a utilizar4>&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.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.Puede canalizar el segundo comando a otro
cat, que esperará hasta que se cierre su canal de entrada. Ex: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 deecho two. Por lo tanto,echo twotermina, y luegoecho threese ejecuta, pero antes de que>(cat)termine. Cuandobashobtiene 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 lomesgeditara).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 paracat. 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 queecho twotermina, A sale, cerrando su stdout. Sin embargo,>(cat)todavía mantiene la referencia al stdin de B. Elcatstdin del segundo contiene el stdin de B, y esocatno 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 segundocat. B sigue esperando ese segundocat. Desde queecho twosalió,>(cat)finalmente obtiene un EOF, así>(cat)descarga su búfer y sale. Ya nadie tiene elcatstdin de B / segundo , por lo que el segundocatlee un EOF (B no está leyendo su stdin en absoluto, no le importa). Este EOF hace que el segundocatvacíe su búfer, cierre su salida estándar y salga, y luego B sale porquecatsalió y B estaba esperandocat.Una advertencia de esto es que bash también genera una subshell para
>(cat)! Debido a esto, verás queecho two > >(sleep 5) | cat; echo threeaún esperará 5 segundos antes de ejecutar
echo three, aunquesleep 5no tenga el stdin de B. Esto se debe a que una subshell oculta C engendrada>(sleep 5)está esperandosleepy C mantiene el stdin de B. Puedes ver comoecho two > >(exec sleep 5) | cat; echo threeSin 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 esperesleep). Independientemente de esta advertencia,echo two > >(exec cat) | cat; echo threeseguirá ejecutando correctamente las funciones en orden, como se describió anteriormente.
fuente
ANo está esperando a loscatengendrados>(cat). Como mencioné en mi respuesta, la razón por la queecho two > >(sleep 5 &>/dev/null) | cat; echo threesalethreedespués de 5 segundos es porque las versiones actuales debashdesperdicio de un proceso de shell adicional en>(sleep 5)esperasleepy ese proceso todavía tiene stdout yendo alpipeque impide que el segundocattermine. Si lo reemplazaecho two > >(exec sleep 5 &>/dev/null) | cat; echo threepara eliminar ese proceso adicional, encontrará que regresa de inmediato.echo two > >(sleep 5 &>/dev/null)como mínimo, tiene su propia subshell. ¿Es un detalle de implementación no documentado que hacesleep 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.$(...), de<(...)hecho, implican una subshell, pero ksh93 o zsh ejecutarían el último comando en esa subshell en el mismo proceso, y nobashes por eso que todavía hay otro proceso que mantiene la tubería abierta mientrassleepse ejecuta y no mantiene la tubería abierta. Las versiones futuras debashpueden implementar una optimización similar.execfunciona como se esperaba.