¿Cómo se usa el comando coproc en varios shells?

Respuestas:

118

los coprocesos son una kshcaracterística (ya está en ksh88). zshha tenido la función desde el principio (principios de los 90), mientras que solo se agregó bashen 4.0(2009).

Sin embargo, el comportamiento y la interfaz son significativamente diferentes entre los 3 shells.

Sin embargo, la idea es la misma: permite iniciar un trabajo en segundo plano y poder enviarle entradas y leer su salida sin tener que recurrir a canalizaciones con nombre.

Eso se hace con tuberías sin nombre con la mayoría de los shells y socketpairs con versiones recientes de ksh93 en algunos sistemas.

En a | cmd | b, aalimenta datos cmdy blee su salida. Ejecutar cmdcomo un coproceso permite que el shell sea ambos ay b.

coprocesos ksh

En ksh, inicia un coproceso como:

cmd |&

Alimenta datos cmdhaciendo cosas como:

echo test >&p

o

print -p test

Y lea cmdla salida con cosas como:

read var <&p

o

read -p var

cmdse inicia como cualquier trabajo en segundo plano, puede utilizar fg, bg, killen él y remitirla por %job-numberoa través $!.

Para cerrar el extremo de escritura de la tubería de la que cmdestá leyendo, puede hacer:

exec 3>&p 3>&-

Y para cerrar el extremo de lectura de la otra tubería (en la que cmdestá escribiendo):

exec 3<&p 3<&-

No puede iniciar un segundo coproceso a menos que primero guarde los descriptores del archivo de tubería en otros archivos. Por ejemplo:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

coprocesos zsh

En zsh, los coprocesos son casi idénticos a los de ksh. La única diferencia real es que los zshcoprocesos se inician con la coprocpalabra clave.

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

Haciendo:

exec 3>&p

Nota: Esto no mueve el coprocdescriptor de archivo a fd 3(como en ksh), sino que lo duplica. Por lo tanto, no hay una forma explícita de cerrar la tubería de alimentación o lectura, otra iniciando otra coproc .

Por ejemplo, para cerrar el extremo de alimentación:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

Además de los coprocesos basados ​​en tuberías, zsh(desde 3.1.6-dev19, lanzado en 2000) tiene construcciones basadas en pseudo-tty como expect. Para interactuar con la mayoría de los programas, los coprocesos de estilo ksh no funcionarán, ya que los programas comienzan a almacenarse en búfer cuando su salida es una tubería.

Aquí hay unos ejemplos.

Comience el coproceso x:

zmodload zsh/zpty
zpty x cmd

(Aquí, cmdes un comando simple. Pero puedes hacer cosas más sofisticadas con evalo funciones).

Alimentar datos de un coproceso:

zpty -w x some data

Leer datos de coprocesos (en el caso más simple):

zpty -r x var

Al igual expect, puede esperar una salida del coproceso que coincida con un patrón dado.

coprocesos bash

La sintaxis de bash es mucho más nueva y se basa en una nueva característica agregada recientemente a ksh93, bash y zsh. Proporciona una sintaxis para permitir el manejo de descriptores de archivo asignados dinámicamente por encima de 10.

bashofrece una sintaxis básica coproc y una sintaxis extendida .

Sintaxis básica

La sintaxis básica para iniciar un coproceso se ve así zsh:

coproc cmd

En ksho zsh, se accede a las tuberías hacia y desde el coproceso con >&py <&p.

Pero en bash, los descriptores de archivo de la tubería del coproceso y la otra tubería al coproceso se devuelven en la $COPROCmatriz (respectivamente ${COPROC[0]}y ${COPROC[1]}. Entonces ...

Alimentar datos al coproceso:

echo xxx >&"${COPROC[1]}"

Leer datos del coproceso:

read var <&"${COPROC[0]}"

Con la sintaxis básica, puede iniciar solo un coproceso a la vez.

Sintaxis Extendida

En la sintaxis extendida, puede nombrar sus coprocesos (como en los zshcoprocesos zpty):

coproc mycoproc { cmd; }

El comando tiene que ser un comando compuesto. (Observe cómo el ejemplo anterior recuerda function f { ...; }).

Esta vez, los descriptores de archivo están en ${mycoproc[0]}y ${mycoproc[1]}.

Puede iniciar más de un compañero de proceso a la vez, pero que hacer una advertencia cuando se inicia un proceso de co-mientras todavía se está ejecutando (incluso en modo no interactivo).

Puede cerrar los descriptores de archivo cuando use la sintaxis extendida.

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

Tenga en cuenta que cerrar de esa manera no funciona en las versiones de bash anteriores a 4.3, donde debe escribirlo en su lugar:

fd=${tr[1]}
exec {fd}>&-

Como en kshy zsh, esos descriptores de archivos de tubería están marcados como close-on-exec.

Pero en bash, la única manera de pasar aquellos a los comandos ejecutados es duplicar a FDS 0, 1, o 2. Eso limita la cantidad de coprocesos con los que puede interactuar para un solo comando. (Consulte a continuación para ver un ejemplo).

proceso yash y redirección de canalización

yashno tiene una función de coproceso per se, pero el mismo concepto se puede implementar con sus características de canalización y redirección de procesos . yashtiene una interfaz para la pipe()llamada al sistema, por lo que este tipo de cosas se pueden hacer relativamente fácilmente a mano allí.

Comenzarías un coproceso con:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

Lo que primero crea un pipe(4,5)(5 el final de la escritura, 4 el final de la lectura), luego redirige fd 3 a una tubería a un proceso que se ejecuta con su stdin en el otro extremo, y stdout va a la tubería creada anteriormente. Luego cerramos el final de escritura de esa tubería en el padre que no necesitaremos. Así que ahora en el shell tenemos fd 3 conectado al std del cmd y fd 4 conectado al stdout del cmd con tuberías.

Tenga en cuenta que el indicador close-on-exec no está establecido en esos descriptores de archivo.

Para alimentar datos:

echo data >&3 4<&-

Para leer datos:

read var <&4 3>&-

Y puedes cerrar fds como de costumbre:

exec 3>&- 4<&-

Ahora, ¿por qué no son tan populares?

casi ningún beneficio sobre el uso de tuberías con nombre

Los coprocesos se pueden implementar fácilmente con canalizaciones con nombre estándar. No sé cuándo se introdujeron las tuberías con nombre exacto, pero es posible que se kshprodujeran coprocesos (probablemente a mediados de los años 80, ksh88 se "lanzó" en 88, pero creo que kshse usó internamente en AT&T unos años antes eso) lo que explicaría por qué.

cmd |&
echo data >&p
read var <&p

Se puede escribir con:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

Interactuar con ellos es más sencillo, especialmente si necesita ejecutar más de un coproceso. (Ver ejemplos a continuación).

El único beneficio del uso coproces que no tiene que limpiar esas tuberías con nombre después del uso.

propenso a un punto muerto

Los proyectiles usan tuberías en algunas construcciones:

  • tuberías de Shell: cmd1 | cmd2 ,
  • comando de sustitución: $(cmd) ,
  • y la sustitución de proceso: <(cmd) , >(cmd).

En esos, los datos fluyen en una sola dirección entre diferentes procesos.

Sin embargo, con coprocesos y canalizaciones con nombre, es fácil encontrarse en un punto muerto. Debe realizar un seguimiento de qué comando tiene qué descriptor de archivo abierto, para evitar que uno permanezca abierto y mantenga vivo un proceso. Los puntos muertos pueden ser difíciles de investigar, ya que pueden ocurrir de manera no determinista; por ejemplo, solo cuando se envían tantos datos como para llenar una tubería.

funciona peor que expectpara lo que ha sido diseñado

El objetivo principal de los coprocesos era proporcionar al shell una forma de interactuar con los comandos. Sin embargo, no funciona tan bien.

La forma más simple de punto muerto mencionada anteriormente es:

tr a b |&
echo a >&p
read var<&p

Debido a que su salida no va a una terminal, tramortigua su salida. Por lo tanto, no generará nada hasta que vea el final del archivo en stdinél o haya acumulado un búfer lleno de datos para generar. Entonces, arriba, después de que el shell haya emitido a\n(solo 2 bytes), readse bloqueará indefinidamente porque trestá esperando que el shell le envíe más datos.

En resumen, las canalizaciones no son buenas para interactuar con los comandos. Los coprocesos solo se pueden usar para interactuar con comandos que no almacenan en memoria intermedia su salida, o comandos a los que se les puede decir que no almacenen en memoria intermedia su salida; por ejemplo, al usar stdbufalgunos comandos en sistemas recientes de GNU o FreeBSD.

Es por eso expecto zptyusar pseudo-terminales en su lugar. expectes una herramienta diseñada para interactuar con comandos, y lo hace bien.

El manejo del descriptor de archivos es complicado y difícil de corregir

Los coprocesos se pueden usar para realizar algunas tuberías más complejas que las que permiten las tuberías de revestimiento simples.

esa otra respuesta de Unix.SE tiene un ejemplo de uso de coproc.

Aquí hay un ejemplo simplificado: imagine que desea una función que alimente una copia de la salida de un comando a otros 3 comandos, y luego haga que la salida de esos 3 comandos se concatene.

Todo utilizando tuberías.

Por ejemplo: alimentar a la salida de la printf '%s\n' foo bara tr a b, sed 's/./&&/g'y cut -b2-obtener algo como:

foo
bbr
ffoooo
bbaarr
oo
ar

Primero, no es necesariamente obvio, pero existe la posibilidad de un punto muerto allí, y comenzará a suceder después de solo unos pocos kilobytes de datos.

Luego, dependiendo de su shell, se encontrará con varios problemas diferentes que deben abordarse de manera diferente.

Por ejemplo, con zsh, lo harías con:

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

Arriba, los fds de coproceso tienen el conjunto de indicadores close-on-exec, pero no los que están duplicados (como en {o1}<&p). Por lo tanto, para evitar puntos muertos, deberá asegurarse de que estén cerrados en cualquier proceso que no los necesite.

Del mismo modo, tenemos que usar un subshell y usarlo exec catal final, para asegurarnos de que no haya un proceso de shell mintiendo sobre mantener una tubería abierta.

Con ksh(aquí ksh93), eso tendría que ser:

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

( Nota: Eso no funcionará en sistemas donde se kshusa en socketpairslugar de pipes, y donde /dev/fd/nfunciona como en Linux).

En ksh, los fds anteriores 2están marcados con el indicador close-on-exec, a menos que se pasen explícitamente en la línea de comando. Es por eso que no tenemos que cerrar los descriptores de archivos no utilizados como con zsh-pero también es por eso que tenemos que hacer {i1}>&$i1y usar evalpara ese nuevo valor de $i1, al pasar a teee cat...

En bashesto no se puede hacer, porque no se puede evitar el cierre-on-exec bandera.

Arriba, es relativamente simple, porque usamos solo comandos externos simples. Se vuelve más complicado cuando quieres usar construcciones de shell allí, y comienzas a encontrarte con errores de shell.

Compare lo anterior con lo mismo usando tuberías con nombre:

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

Conclusión

Si desea interactuar con un comando, use expecto zsh's zptyo canalizaciones con nombre.

Si desea hacer una fontanería elegante con tuberías, use tuberías con nombre.

Los coprocesos pueden hacer algo de lo anterior, pero prepárate para rascarte la cabeza seriamente para cualquier cosa que no sea trivial.

Stéphane Chazelas
fuente
Gran respuesta de hecho. No sé cuándo se solucionó específicamente, pero al menos bash 4.3.11, ahora puede cerrar los descriptores de archivos coproc directamente, sin la necesidad de un auxiliar. variable; en términos del ejemplo en su respuesta exec {tr[1]}<&- ahora funcionaría (para cerrar el stdin del coproc; tenga en cuenta que su código (indirectamente) intenta cerrar {tr[1]}usando >&-, pero {tr[1]}es el stdin del coproc , y debe cerrarse con <&-). La solución debe haber estado en algún punto intermedio 4.2.25, que aún muestra el problema y 4.3.11que no.
mklement0
1
@ mklement0, gracias. exec {tr[1]}>&-de hecho parece funcionar con versiones más nuevas y se hace referencia en una entrada CWRU / changelog ( permitir que palabras como {array [ind]} sean redireccionamientos válidos ... 2012-09-01). exec {tr[1]}<&-(o el >&-equivalente más correcto, aunque eso no hace ninguna diferencia, ya que solo requiere close()ambos) no cierra el stdin del coproc, sino el final de la escritura de la tubería a ese coproc.
Stéphane Chazelas
1
@ mklement0, buen punto, lo actualicé y agregué yash.
Stéphane Chazelas
1
Una ventaja mkfifoes que no tiene que preocuparse por las condiciones de carrera y la seguridad del acceso a la tubería. Todavía tienes que preocuparte por un punto muerto con fifo.
Oteo
1
Acerca de los puntos muertos: el stdbufcomando puede ayudar a prevenir al menos algunos de ellos. Lo usé en Linux y bash. De todos modos, creo que @ StéphaneChazelas tiene razón en la conclusión: la fase de "rascarse la cabeza" terminó para mí solo cuando volví a las tuberías con nombre.
enviado
7

Los coprocesos se introdujeron por primera vez en un lenguaje de script de shell con el ksh88shell (1988), y más tarde en zshalgún momento antes de 1993.

La sintaxis para iniciar un coproceso en ksh es command |&. A partir de ahí, puede escribir en commandla entrada estándar con print -py leer su salida estándar con read -p.

Más de un par de décadas después, bash, que carecía de esta característica, finalmente la introdujo en su versión 4.0. Desafortunadamente, se seleccionó una sintaxis incompatible y más compleja.

En bash 4.0 y versiones posteriores, puede iniciar un coproceso con el coproccomando, por ejemplo:

$ coproc awk '{print $2;fflush();}'

Luego puede pasar algo al comando stdin de esa manera:

$ echo one two three >&${COPROC[1]}

y lee la salida awk con:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

Bajo ksh, eso habría sido:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
jlliagre
fuente
-1

¿Qué es un "coproc"?

Es la abreviatura de "coproceso", que significa un segundo proceso que coopera con el shell. Es muy similar a un trabajo en segundo plano que comenzó con un "&" al final del comando, excepto que en lugar de compartir la misma entrada y salida estándar que su shell principal, su E / S estándar está conectada al shell principal mediante un especial tipo de tubería llamada FIFO. Para referencia, haga clic aquí

Uno comienza un coproc en zsh con

coproc command

El comando debe estar preparado para leer desde stdin y / o escribir en stdout, o no es de mucha utilidad como coproc.

Lea este artículo aquí , proporciona un estudio de caso entre exec y coproc

Munai Das Udasin
fuente
¿Puedes agregar algo del artículo a tu respuesta? Estaba tratando de cubrir este tema en U&L ya que parecía poco representado. ¡Gracias por tu respuesta! Observe también que configuré la etiqueta como Bash, no como zsh.
slm
@slm Ya apuntaste a los hackers de Bash. Vi allí suficientes ejemplos. Si su intención era llamar la atención sobre esta pregunta, entonces sí tuvo éxito:>
Valentin Bajrami
No son tipos especiales de tuberías, son las mismas tuberías que se usan con |. (es decir, usar tuberías en la mayoría de los depósitos y pares de enchufes en ksh93). Las tuberías y los pares de enchufes son primero en entrar, primero en salir, todos son FIFO. mkfifohace tuberías con nombre, los coprocesos no usan tuberías con nombre.
Stéphane Chazelas
@slm lo siento por zsh ... en realidad trabajo en zsh. Tiendo a hacerlo a veces con la corriente. También funciona bien en Bash ...
Munai Das Udasin
@ Stephane Chazelas Estoy bastante seguro de que lo leí en alguna parte que es E / S está conectado con tipos especiales de tuberías llamadas FIFO ...
Munai Das Udasin
-1

Aquí hay otro buen ejemplo (y funciona): un servidor simple escrito en BASH. Tenga en cuenta que necesitaría OpenBSD netcat, el clásico no funcionará. Por supuesto, podría usar un socket inet en lugar de unix.

server.sh:

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh:

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

Uso:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
Alexey Naidyonov
fuente