Como fazer tubo bidirecional entre dois programas?

63

Todo mundo sabe como fazer tubo unidirecional entre dois programas (ligamento stdoutde primeiro e stdinde segundo): first | second.

Mas como fazer tubo bidirecional, ou seja, ligação cruzada stdine stdoutde dois programas? Existe uma maneira fácil de fazer isso em uma concha?


fonte

Respostas:

30

Se os pipes no seu sistema forem bidirecionais (como no Solaris 11 e em alguns BSDs, pelo menos, mas não no Linux):

cmd1 <&1 | cmd2 >&0

Cuidado com os impasses.

Observe também que algumas versões do ksh93 em alguns sistemas implementam pipes ( |) usando um par de soquetes . pares de soquete são bidirecionais, mas o ksh93 desativa explicitamente a direção reversa, portanto, o comando acima não funcionaria com esses ksh93s mesmo em sistemas em que os tubos (criados pela pipe(2)chamada do sistema) são bidirecionais.

Stéphane Chazelas
fonte
11
alguém sabe se isso funciona no linux também? (usando archlinux aqui)
heinrich5991
41

Bem, é bastante "fácil" com pipes nomeados ( mkfifo). Coloquei aspas fáceis porque, a menos que os programas sejam projetados para isso, é provável que haja um impasse.

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

Agora, normalmente há buffer envolvido na gravação de stdout. Então, por exemplo, se os dois programas fossem:

#!/usr/bin/perl
use 5.010;
say 1;
print while (<>);

você esperaria um loop infinito. Mas, em vez disso, ambos entrariam em impasse; você precisaria adicionar $| = 1(ou equivalente) para desativar o buffer de saída. O impasse é causado porque os dois programas estão aguardando algo no stdin, mas não o estão vendo porque está no buffer stdout do outro programa e ainda não foi gravado no canal.

Atualização : incorporando sugestões de Stéphane Charzelas e Joost:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

faz o mesmo, é mais curto e mais portátil.

derobert
fonte
23
Um pipe nomeado é suficiente: prog1 < fifo | prog2 > fifo.
Andrey Vihrov
2
@AndreyVihrov, é verdade, você pode substituir um canal anônimo por um dos nomeados. Mas eu gosto da simetria:
P
3
@ user14284: No Linux, você provavelmente pode fazê-lo com algo parecido prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo.
Andrey Vihrov 6/11/12
3
Se você conseguir prog2 < fifo0 > fifo1, você pode evitar a sua pequena dança com exec 30< ...(que por sinal só funciona com bashou yashpara fds acima de 10 assim).
Stéphane Chazelas
11
@ Joost Hmmm, parece que você está certo de que não há necessidade, pelo menos no bash. Eu provavelmente estava preocupado que, já que o shell executa os redirecionamentos (incluindo a abertura dos tubos), ele pode bloquear - mas pelo menos soltar garfos antes de abrir o fifos. dashparece OK também (mas comporta-se um pouco diferente)
derobert
13

Não tenho certeza se é isso que você está tentando fazer:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

Isso começa abrindo um soquete de escuta na porta 8096 e, uma vez que uma conexão é estabelecida, gera um programa secondcom stdina saída de fluxo e stdouta entrada de fluxo.

Em seguida, uma segunda ncé lançado, que conecta-se à porta de escuta e gera programa firstcom o stdoutque a entrada corrente e a sua stdincomo o fluxo de saída.

Isso não é exatamente feito usando um pipe, mas parece fazer o que você precisa.

Como isso usa a rede, isso pode ser feito em 2 computadores remotos. É quase assim que um servidor web ( second) e um navegador web ( first) funcionam.

jfg956
fonte
11
E nc -Upara soquetes de domínio UNIX que ocupam apenas espaço de endereço do sistema de arquivos.
Ciro Santilli publicou
A opção -c não está disponível no Linux. Minha felicidade foi curta :-(
pablochacin 30/03
9

Você pode usar o pipexec :

$ pipexec -- '[A' cmd1 ] '[B' cmd2 ] '{A:1>B:0}' '{B:1>A:0}'
Andreas Florath
fonte
6

basha versão 4 possui um coproccomando que permite que isso seja feito de forma pura, bashsem pipes nomeados:

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

Algumas outras conchas também podem funcionar coproc.

Abaixo está uma resposta mais detalhada, mas encadeia três comandos, em vez de dois, o que torna um pouco mais interessante.

Se você estiver feliz em usar cate stdbufconstruir, pode ser mais fácil de entender.

Versão usando bashcom cate stdbuffácil de entender:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

Observe que é necessário usar eval porque a expansão variável em <& $ var é ilegal no meu versio do bash 4.2.25.

Versão usando pure bash: Divida em duas partes, inicie o primeiro pipeline sob coproc e depois alimente a segunda parte ((um único comando ou um pipeline) reconectando-o à primeira:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

Prova de conceito:

arquivo ./prog, apenas um programa fictício para consumir, marcar e reimprimir linhas. Usar sub-conchas para evitar problemas de buffer talvez exagere, não é o ponto aqui.

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

file ./start_cat Esta é uma versão usando bash, catestdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

ou arquivo ./start_part. Esta é uma versão usando bashapenas puro . Para fins de demonstração, ainda estou usando, stdbufporque seu programa real precisaria lidar com o buffer internamente de qualquer maneira, para evitar o bloqueio devido ao buffer.

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

Resultado:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

Isso faz.

AnyDev
fonte
5

Um bloco de construção conveniente para escrever esses tubos bidirecionais é algo que conecta o stdout e o stdin do processo atual. Vamos chamá-lo de ioloop. Depois de chamar esta função, você só precisa iniciar um canal normal:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

Se você não deseja modificar os descritores do shell de nível superior, execute isso em um subshell:

( ioloop && cmd1 | cmd2 )

Aqui está uma implementação portátil do ioloop usando um pipe nomeado:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

O pipe nomeado existe no sistema de arquivos apenas brevemente durante a configuração do ioloop. Esta função não é bem POSIX porque o mktemp está obsoleto (e potencialmente vulnerável a um ataque de corrida).

É possível uma implementação específica do linux usando / proc / que não exija um pipe nomeado, mas acho que este é mais que suficiente.

user873942
fonte
Função interessante, +1. Provavelmente poderia usar uma frase ou 2 adicionadas explicando ( : <$FIFO & )com mais detalhes. Obrigado por postar.
Alex Stragies
Eu olhei em volta e fiquei em branco; onde posso encontrar informações sobre a suspensão de uso mktemp? Eu o uso extensivamente e, se uma ferramenta mais recente tiver sido substituída, gostaria de começar a usá-la.
DopeGhoti
Alex: o syscall aberto (2) em um tubo naned está bloqueando. se você tentar "exec <$ PIPE> $ PIPE", ele ficará preso aguardando que outro processo abra o outro lado. O comando ": <$ FIFO &" é executado em um subshell em segundo plano e permite que o redirecionamento bidirecional seja concluído com êxito.
user873942
DopeGhoti: a função da biblioteca mktemp (3) C está obsoleta. O utilitário mktemp (1) não é.
user873942
4

Há também

Como @ StéphaneChazelas observa corretamente nos comentários, os exemplos acima são o "formulário base", ele tem bons exemplos com opções em sua resposta para uma pergunta semelhante .

Alex Stragies
fonte
Observe que, por padrão, socatusa soquetes em vez de tubos (você pode alterar isso com commtype=pipes). Você pode adicionar a noforkopção para evitar um processo socat extra que empurra dados entre os tubos / soquetes. (obrigado pela edição na minha resposta )
Stéphane Chazelas
0

Há muitas ótimas respostas aqui. Então, eu só quero adicionar algo para facilitar a brincadeira com eles. Presumo que stderrnão seja redirecionado para lugar nenhum. Crie dois scripts (digamos a.sh e b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

Então, quando você os conectar da melhor maneira possível, você verá no console:

1 ./a.sh got: bar
1 ./b.sh got: foo
2 ./a.sh got: foo
2 ./b.sh got: bar
3 ./a.sh got: bar
3 ./b.sh got: foo
4 ./a.sh got: foo
4 ./b.sh got: bar
...
pawel7318
fonte