Como você usa o comando coproc em várias conchas?


Respostas:


118

co-processos são um kshrecurso (já incluído ksh88). zshteve o recurso desde o início (início dos anos 90), enquanto apenas foi adicionado bashem 4.0(2009).

No entanto, o comportamento e a interface são significativamente diferentes entre as três conchas.

A idéia é a mesma: permite iniciar um trabalho em segundo plano e poder enviar entrada e ler sua saída sem precisar recorrer a pipes nomeados.

Isso é feito com pipes não nomeados com a maioria dos shells e socketpairs com versões recentes do ksh93 em alguns sistemas.

Em a | cmd | b, aalimenta os dados cmde blê sua saída. A execução cmdcomo um co-processo permite que o shell seja ambos ae b.

co-processos ksh

Em ksh, você inicia um coprocesso como:

cmd |&

Você alimenta os dados cmdfazendo coisas como:

echo test >&p

ou

print -p test

E leia cmda saída com coisas como:

read var <&p

ou

read -p var

cmdé iniciado como qualquer trabalho de fundo, você pode usar fg, bg, killnele e apresentá-lo por %job-numberou via $!.

Para fechar a extremidade de gravação do pipe cmd, você pode fazer:

exec 3>&p 3>&-

E para fechar a extremidade de leitura do outro canal (aquele que cmdestá escrevendo):

exec 3<&p 3<&-

Você não pode iniciar um segundo co-processo, a menos que primeiro salve os descritores de arquivo de pipe em outros fds. Por exemplo:

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

co-processos zsh

Em zsh, co-processos são quase idênticos aos de ksh. A única diferença real é que os zshco-processos são iniciados com a coprocpalavra - chave.

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

Fazendo:

exec 3>&p

Nota: Isso não move o coprocdescritor de arquivo para fd 3(como em ksh), mas o duplica. Portanto, não há uma maneira explícita de fechar o tubo de alimentação ou de leitura, outro iniciando outro coproc .

Por exemplo, para fechar o final da alimentação:

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

Além zshdos coprocessos baseados em pipe, (desde 3.1.6-dev19, lançado em 2000), tem construções baseadas em pseudo-tty como expect. Para interagir com a maioria dos programas, os co-processos no estilo ksh não funcionam, pois os programas começam a armazenar em buffer quando sua saída é um canal.

Aqui estão alguns exemplos.

Inicie o co-processo x:

zmodload zsh/zpty
zpty x cmd

(Aqui cmdestá um comando simples. Mas você pode fazer coisas mais sofisticadas com evalou funções.)

Alimente dados de um co-processo:

zpty -w x some data

Leia os dados de co-processo (no caso mais simples):

zpty -r x var

Assim expect, ele pode esperar por alguma saída do co-processo que corresponda a um determinado padrão.

co-processos do bash

A sintaxe do bash é muito mais recente e se baseia em um novo recurso adicionado recentemente ao ksh93, bash e zsh. Ele fornece uma sintaxe para permitir o tratamento de descritores de arquivos alocados dinamicamente acima de 10.

bashoferece uma sintaxe básica coproc e uma extendida .

Sintaxe básica

A sintaxe básica para iniciar um co-processo é semelhante zsha:

coproc cmd

Em kshou zsh, os tubos de e para o co-processo são acessados ​​com >&pe <&p.

Porém bash, em , os descritores de arquivo do canal do co-processo e do outro canal para o co-processo são retornados na $COPROCmatriz (respectivamente ${COPROC[0]}e ${COPROC[1]}

Alimente os dados para o co-processo:

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

Leia os dados do co-processo:

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

Com a sintaxe básica, você pode iniciar apenas um co-processo por vez.

Sintaxe estendida

Na sintaxe estendida, você pode nomear seus co-processos (como nos zshco-processos zpty):

coproc mycoproc { cmd; }

O comando deve ser um comando composto. (Observe como o exemplo acima é uma reminiscência de function f { ...; }.)

Desta vez, os descritores de arquivo estão em ${mycoproc[0]}e ${mycoproc[1]}.

Você pode começar mais de um co-processo de cada vez, mas você fazer receber um aviso quando você iniciar um co-processo, enquanto um ainda está em execução (mesmo em modo não-interativo).

Você pode fechar os descritores de arquivo ao usar a sintaxe estendida.

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

exec {tr[1]}>&-

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

Observe que o fechamento dessa maneira não funciona nas versões bash anteriores à 4.3, nas quais você deve escrevê-lo:

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

Como em kshe zsh, esses descritores de arquivo de pipe são marcados como close-on-exec.

Mas, em bash, a única maneira de passar aqueles aos comandos executados é duplicar-los para fds 0, 1, ou 2. Isso limita o número de co-processos com os quais você pode interagir para um único comando. (Veja abaixo um exemplo.)

processo yash e redirecionamento de pipeline

yashnão possui um recurso de co-processo em si, mas o mesmo conceito pode ser implementado com seus recursos de redirecionamento de pipeline e processo . yashpossui uma interface para a pipe()chamada do sistema, para que esse tipo de coisa possa ser feita com relativa facilidade à mão.

Você iniciaria um co-processo com:

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

O que primeiro cria um pipe(4,5)(5 no final da gravação, 4 no final da leitura) e depois redireciona o fd 3 para um canal para um processo que é executado com seu stdin na outra extremidade e stdout para o canal criado anteriormente. Em seguida, fechamos a extremidade de escrita desse tubo no pai, da qual não precisaremos. Então agora no shell temos o fd 3 conectado ao stdin do cmd e o fd 4 conectado ao stdout do cmd com pipes.

Observe que o sinalizador close-on-exec não está definido nesses descritores de arquivo.

Para alimentar dados:

echo data >&3 4<&-

Para ler dados:

read var <&4 3>&-

E você pode fechar o fds como de costume:

exec 3>&- 4<&-

Agora, por que eles não são tão populares

quase nenhum benefício sobre o uso de pipes nomeados

Co-processos podem ser facilmente implementados com pipes nomeados padrão. Não sei quando foram introduzidos exatamente os pipes nomeados, mas é possível que tenha kshsurgido com co-processos (provavelmente em meados dos anos 80, o ksh88 foi "lançado" em 88, mas acredito que kshfoi usado internamente na AT&T alguns anos antes isso), o que explicaria o porquê.

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

Pode ser escrito com:

mkfifo in out

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

A interação com essas pessoas é mais direta - especialmente se você precisar executar mais de um co-processo. (Veja exemplos abaixo.)

O único benefício do uso coprocé que você não precisa limpar os tubos nomeados após o uso.

propenso a impasses

Os reservatórios usam tubos em algumas construções:

  • tubos de shell: cmd1 | cmd2 ,
  • substituição de comando: $(cmd) ,
  • e substituição de processo: <(cmd) , >(cmd).

Nesses, os dados fluem em apenas uma direção entre diferentes processos.

Com co-processos e pipes nomeados, porém, é fácil encontrar um impasse. Você deve acompanhar qual comando possui qual descritor de arquivo aberto, para impedir que um permaneça aberto e mantenha um processo ativo. Os conflitos podem ser difíceis de investigar, porque podem ocorrer de forma não determinística; por exemplo, apenas quando são enviados tantos dados quanto para encher um tubo.

funciona pior do que expectpara o que foi projetado

O principal objetivo dos co-processos era fornecer ao shell uma maneira de interagir com os comandos. No entanto, não funciona tão bem.

A forma mais simples de conflito mencionada acima é:

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

Como a saída não vai para um terminal, tra saída é armazenada em buffer. Portanto, ele não produzirá nada até que veja o final do arquivo stdinou tenha acumulado um buffer cheio de dados para a saída. Assim, acima, após a saída do shell a\n(apenas 2 bytes), o readbloco será bloqueado indefinidamente porque traguarda o envio de mais dados pelo shell.

Em resumo, os pipes não são bons para interagir com os comandos. Co-processos só podem ser usados ​​para interagir com comandos que não armazenam em buffer sua saída ou comandos que podem ser instruídos a não armazenar em buffer sua saída; por exemplo, usando stdbufcom alguns comandos nos sistemas recentes GNU ou FreeBSD.

Por isso, expectou zptyuse pseudo-terminais. expecté uma ferramenta projetada para interagir com comandos e funciona bem.

O manuseio do descritor de arquivos é complicado e difícil de corrigir

Os coprocessos podem ser usados ​​para fazer um encanamento mais complexo do que o que os tubos simples da concha permitem.

essa outra resposta do Unix.SE tem um exemplo de uso coproc.

Aqui está um exemplo simplificado: imagine que você deseja uma função que alimente uma cópia da saída de um comando para outros 3 comandos e faça com que a saída desses 3 comandos seja concatenada.

Todos usando canos.

Por exemplo: alimentar a saída printf '%s\n' foo barpara tr a b, sed 's/./&&/g'e cut -b2-obter algo como:

foo
bbr
ffoooo
bbaarr
oo
ar

Primeiro, não é necessariamente óbvio, mas há uma possibilidade de impasse por lá, e isso começará a acontecer após apenas alguns kilobytes de dados.

Então, dependendo do seu shell, você terá vários problemas diferentes que precisam ser tratados de maneira diferente.

Por exemplo, com zsh, você faria isso com:

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

Acima, o co-processo fds tem o sinalizador close-on-exec definido, mas não os duplicados (como em {o1}<&p). Portanto, para evitar conflitos, você deve garantir que eles sejam fechados em qualquer processo que não precise deles.

Da mesma forma, temos que usar um subshell e usar exec catno final, para garantir que não exista nenhum processo de shell sobre manter um tubo aberto.

Com ksh(aqui ksh93), isso teria 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: Isso não funcionará em sistemas onde kshusa , e socketpairsnão pipes, e onde /dev/fd/nfunciona como no Linux.)

Em ksh, fds acima 2são marcados com o sinalizador close-on-exec, a menos que sejam passados ​​explicitamente na linha de comando. É por isso que não precisamos fechar os descritores de arquivos não utilizados como com zsh- mas também é por isso que precisamos fazer {i1}>&$i1e usar evalesse novo valor de $i1, a ser passado teee cat

Em bashisso não pode ser feito, porque você não pode evitar o close-on-exec bandeira.

Acima, é relativamente simples, porque usamos apenas comandos externos simples. Torna-se mais complicado quando você deseja usar construções de shell lá, e você começa a encontrar bugs de shell.

Compare o acima com o mesmo usando pipes nomeados:

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

Conclusão

Se você deseja interagir com um comando, use expectou zsh's zptyou pipes nomeados.

Se você quiser fazer um encanamento sofisticado com canos, use canos nomeados.

Os co-processos podem fazer algumas das opções acima, mas esteja preparado para fazer algo sério na cabeça por algo não trivial.


Ótima resposta, de fato. Não sei quando foi corrigido especificamente, mas, pelo menos bash 4.3.11, agora você pode fechar os descritores de arquivos coproc diretamente, sem a necessidade de um aux. variável; exec {tr[1]}<&- Agora, em termos do exemplo em sua resposta , funcionaria (para fechar o stdin do coproc; observe que seu código (indiretamente) tenta fechar {tr[1]}usando >&-, mas {tr[1]}é o stdin do coproc e deve ser fechado com <&-). A correção deve ter chegado a um ponto intermediário 4.2.25, que ainda apresenta o problema e 4.3.11, o que não ocorre.
precisa saber é o seguinte

1
@ mklement0, obrigado. exec {tr[1]}>&-realmente parece funcionar com versões mais recentes e é referenciado em uma entrada CWRU / changelog ( permita que palavras como {array [ind]} sejam redirecionamento válido ... 2012-09-01). exec {tr[1]}<&-(ou o >&-equivalente mais correto, porém, que não faz diferença, pois isso exige apenas os close()dois) não fecha o stdin do coproc, mas a extremidade de gravação do pipe nesse coproc.
Stéphane Chazelas

1
@ mklement0, bom ponto, eu atualizei e adicionei yash.
Stéphane Chazelas

1
Uma vantagem mkfifoé que você não precisa se preocupar com as condições da corrida e a segurança do acesso ao tubo. Você ainda precisa se preocupar com o impasse com o fifo.
Otheus 19/11

1
Sobre conflitos: o stdbufcomando pode ajudar a impedir pelo menos alguns deles. Eu usei no Linux e bash. De qualquer forma, acredito que o @ StéphaneChazelas está certo na Conclusão: a fase de "coçar a cabeça" só terminou para mim quando retornei aos pipes nomeados.
Shub

7

Os co-processos foram introduzidos pela primeira vez em uma linguagem de script de shell com o ksh88shell (1988) e, posteriormente, em zshalgum momento antes de 1993.

A sintaxe para iniciar um co-processo no ksh é command |&. A partir daí, você pode gravar na commandentrada padrão print -pe ler sua saída padrão com read -p.

Mais de duas décadas depois, o bash, que não possuía esse recurso, finalmente o introduziu na versão 4.0. Infelizmente, uma sintaxe incompatível e mais complexa foi selecionada.

No bash 4.0 e mais recente, você pode iniciar um co-processo com o coproccomando, por exemplo:

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

Você pode então passar algo para o comando stdin dessa maneira:

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

e leia a saída do awk com:

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

Sob o ksh, isso teria sido:

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

-1

O que é um "coproc"?

É a abreviação de "co-processo", o que significa um segundo processo que coopera com o shell. É muito semelhante a um trabalho em segundo plano iniciado com um "&" no final do comando, exceto que, em vez de compartilhar a mesma entrada e saída padrão que seu shell pai, sua E / S padrão é conectada ao shell pai por um especial tipo de tubo chamado FIFO. Para referência, clique aqui

Um inicia um coproc no zsh com

coproc command

O comando deve estar preparado para ler do stdin e / ou gravar no stdout, ou não é muito útil como um coproc.

Leia este artigo aqui , fornece um estudo de caso entre exec e coproc


Você pode adicionar parte do artigo à sua resposta? Eu estava tentando obter esse tópico coberto em U&L, pois parecia sub-representado. Obrigado pela sua resposta! Observe também que eu defino a tag como Bash, não zsh.
slm

@slm Você já apontou para os hackers do Bash. Eu vi exemplos suficientes. Se a sua intenção era trazer esta questão sob atenção, então sim, você conseguiu:>
Valentin Bajrami

Eles não são tipos especiais de tubos, são os mesmos que são usados ​​com eles |. (ou seja, use tubos na maioria das conchas e pares de soquetes no ksh93). tubos e pares de soquetes são os primeiros a entrar, os primeiros a sair, todos são FIFO. mkfifofaz pipes nomeados, coprocessos não usam pipes nomeados.
Stéphane Chazelas

@ slm desculpe pelo zsh ... na verdade eu trabalho no zsh. Eu costumo fazer isso algumas vezes com o fluxo. Ele funciona muito bem em Bash também ...
Munai Das Udasin

@ Stephane Chazelas eu tenho quase certeza que eu li em algum lugar que é I / O está conectado com tipos especiais de tubos chamados FIFO ...
Munai Das Udasin

-1

Aqui está outro exemplo bom (e funcional) - um servidor simples escrito em BASH. Observe que você precisaria do OpenBSD netcat, o clássico não funcionará. Claro que você pode usar o inet socket em vez do unix one.

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)
$
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.