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 seq
salida a la cat
entrada 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 bash
funciona bajo el capó).
bash
process-substitution
MiniMax
fuente
fuente
Respuestas:
Sí, al
bash
igual 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
cmd1
ycmd1
típicamente estará esperando encmd2
virtud de leer hasta el final del archivo en la tubería que se sustituye, y ese final del archivo generalmente ocurre cuandocmd2
muere. Esa es la misma razón varios proyectiles (nobash
) no se molestan en espera decmd2
encmd2 | cmd1
.Para
cmd1 >(cmd2)
, sin embargo, eso no es generalmente el caso, ya que es máscmd2
que normalmente espera a quecmd1
no lo hará por lo general después de la salida.Eso se solucionó en las
zsh
esperascmd2
allí (pero no si lo escribe comocmd1 > >(cmd2)
ycmd1
no está integrado, use{cmd1} > >(cmd2)
en su lugar como se documenta ).ksh
no espera de forma predeterminada, pero le permite esperar con elwait
incorporado (también hace que el pid esté disponible$!
, aunque eso no ayuda si lo hacecmd1 >(cmd2) >(cmd3)
)rc
(con lacmd1 >{cmd2}
sintaxis), igual que,ksh
excepto que puede obtener los pids de todos los procesos en segundo plano$apids
.es
(también concmd1 >{cmd2}
) esperacmd2
como enzsh
, y también espera redireccionescmd2
en<{cmd2}
proceso.bash
hace que el pid decmd2
(o más exactamente de la subshell ya que se ejecutacmd2
en 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
cmd1
ycmd2
tener su fd 3 abierta a una tubería.cat
esperará al final de su archivo en el otro extremo, por lo general sólo se salga cuando amboscmd1
ycmd2
están muertos. Y el shell esperará esecat
comando. 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
cmd2
cierra su fd 3 (los comandos generalmente no hacen eso, pero a algunos les gustasudo
o lossh
hacen). Las versiones futuras debash
eventualmente 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
sudo
comando.Tenga en cuenta que
cat
no 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
cat
utilizando una sustitución de comando para realizar la sincronización de la tubería:Esta vez, es el caparazón en lugar de lo
cat
que está leyendo desde la tubería cuyo otro extremo está abierto en fd 3 decmd1
ycmd2
. Estamos utilizando una asignación variable para que el estado de salida decmd1
esté disponible en$?
.O podría hacer la sustitución del proceso a mano, y luego incluso podría usar el sistema,
sh
ya 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
sh
implementaciones esperaríancmd1
después de quecmd2
haya terminado (aunque eso es mejor que al revés). Ese tiempo,$?
contiene el estado de salida decmd2
; sin embargo,bash
yzsh
hacer quecmd1
el estado de salida esté disponible en${PIPESTATUS[0]}
y$pipestatus[1]
respectivamente (consulte también lapipefail
opció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
yash
tiene problemas similares con su función de redirección de procesos .cmd1 >(cmd2)
Sería escritocmd1 /dev/fd/3 3>(cmd2)
allí. Perocmd2
no se espera y tampoco se puede usarwait
para 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 redirigidosstdout
al 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?cmd1
ycmd2
el 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>&1
crea 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>&1
hace que fd 3 se conecte al stdin de los brackets externos.>&4
hace 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>&1
se 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>5
significa "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 two
termina, y luegoecho three
se ejecuta, pero antes de que>(cat)
termine. Cuandobash
obtiene 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 lomesg
editara).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 two
y>(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 two
termina, A sale, cerrando su stdout. Sin embargo,>(cat)
todavía mantiene la referencia al stdin de B. Elcat
stdin del segundo contiene el stdin de B, y esocat
no 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 two
salió,>(cat)
finalmente obtiene un EOF, así>(cat)
descarga su búfer y sale. Ya nadie tiene elcat
stdin de B / segundo , por lo que el segundocat
lee un EOF (B no está leyendo su stdin en absoluto, no le importa). Este EOF hace que el segundocat
vacíe su búfer, cierre su salida estándar y salga, y luego B sale porquecat
salió 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 three
aún esperará 5 segundos antes de ejecutar
echo three
, aunquesleep 5
no tenga el stdin de B. Esto se debe a que una subshell oculta C engendrada>(sleep 5)
está esperandosleep
y C mantiene el stdin de B. Puedes ver comoecho two > >(exec sleep 5) | cat; echo three
Sin embargo, no esperará, ya
sleep
que 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 three
seguirá ejecutando correctamente las funciones en orden, como se describió anteriormente.
fuente
A
No está esperando a loscat
engendrados>(cat)
. Como mencioné en mi respuesta, la razón por la queecho two > >(sleep 5 &>/dev/null) | cat; echo three
salethree
después de 5 segundos es porque las versiones actuales debash
desperdicio de un proceso de shell adicional en>(sleep 5)
esperasleep
y ese proceso todavía tiene stdout yendo alpipe
que impide que el segundocat
termine. Si lo reemplazaecho two > >(exec sleep 5 &>/dev/null) | cat; echo three
para 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 5
que 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 nobash
es por eso que todavía hay otro proceso que mantiene la tubería abierta mientrassleep
se ejecuta y no mantiene la tubería abierta. Las versiones futuras debash
pueden implementar una optimización similar.exec
funciona como se esperaba.