Os coprocessos no Bash são uma funcionalidade avançada que permite iniciar um comando em segundo plano e estabelecer uma comunicação bidirecional com ele. Enquanto um pipeline tradicional conecta a saída de um comando à entrada de outro, um coprocesso mantém ambos os canais abertos, permitindo que o script envie dados para o processo e receba respostas simultaneamente. Isso é extremamente útil em cenários onde você precisa de interação contínua com um programa, como um servidor, um interpretador interativo ou um filtro que processa dados sob demanda.

Nesta aula, vamos mergulhar na sintaxe e no funcionamento dos coprocessos, explorar exemplos práticos e discutir quando utilizá-los. Você verá como criar um coprocesso com a palavra-chave `coproc`, como acessar os file descriptors de entrada e saída, e como gerenciar a comunicação de forma eficiente. Além disso, abordaremos as limitações dessa técnica, como o suporte a apenas um coprocesso por vez e a falta de sincronização automática.

coproc

A palavra-chave coproc é usada para iniciar um comando como um coprocesso. A sintaxe básica é:

coproc [NOME] comando [argumentos...]

Se você fornecer um NOME, o Bash criará um array associativo com esse nome contendo os file descriptors. Se não fornecer, o nome padrão será COPROC. O coprocesso é executado em segundo plano, e o script pode se comunicar com ele através dos file descriptors ${NOME[0]} (para leitura da saída do coprocesso) e ${NOME[1]} (para escrita na entrada do coprocesso).

Vamos a um exemplo simples. Suponha que você queira executar o comando bc (uma calculadora de precisão arbitrária) como um coprocesso para fazer cálculos interativamente:

#!/bin/bash

# Inicia o coprocesso 'calc' com o comando bc
coproc CALC { bc; }

# Envia um comando para o bc
echo "3 + 4" >&${CALC[1]}

# Lê a resposta
read -u ${CALC[0]} resultado

# Exibe o resultado
echo "Resultado: $resultado"

# Encerra o coprocesso
echo "quit" >&${CALC[1]}
wait $CALC_PID

Neste exemplo, coproc CALC { bc; } inicia o comando bc como um coprocesso. O array CALC conterá os file descriptors: ${CALC[0]} é o descritor de leitura (saída do bc) e ${CALC[1]} é o descritor de escrita (entrada do bc). A variável CALC_PID é automaticamente definida com o PID do processo. Enviamos a expressão 3 + 4 para o bc, lemos a resposta com read -u ${CALC[0]}, e depois enviamos quit para encerrar o coprocesso.

Observe que precisamos usar >&${CALC[1]} para redirecionar a saída do nosso script para o descritor de escrita. Isso é semelhante ao redirecionamento para um arquivo, mas para um descritor de arquivo específico. Da mesma forma, read -u ${CALC[0]} lê de um descritor de arquivo específico.

Comunicação bidirecional

A principal vantagem dos coprocessos é a comunicação bidirecional. Diferente de um pipe simples, onde os dados fluem em uma direção, o coprocesso permite tanto enviar quanto receber dados. Isso é possível porque o Bash cria dois pipes: um para a entrada do coprocesso e outro para a saída. O script mantém os descritores de ambos os pipes abertos.

Vamos explorar um exemplo mais elaborado: um servidor de eco que recebe linhas e as devolve em maiúsculas. Podemos usar o comando tr como coprocesso, mas tr não é interativo, então não é adequado. Em vez disso, podemos usar um script simples ou um comando como cat que repete o que recebe. Para um exemplo mais real, vamos usar o comando python3 com um script que lê linhas e as imprime em maiúsculas:

#!/bin/bash

# Inicia um coprocesso com Python
coproc PY { python3 -c "
import sys
for line in sys.stdin:
    sys.stdout.write(line.upper())
    sys.stdout.flush()
" ; }

# Envia algumas linhas
for i in 1 2 3; do
    echo "linha $i" >&${PY[1]}
    read -u ${PY[0]} resposta
    echo "Resposta: $resposta"
done

# Encerra o coprocesso
echo "" >&${PY[1]}
wait $PY_PID

Neste exemplo, o coprocesso Python lê do stdin, converte para maiúsculas e escreve no stdout. O script envia três linhas e lê a resposta de cada uma. Note que usamos flush() no Python para garantir que a saída seja enviada imediatamente, caso contrário, o buffer pode atrasar a comunicação.

É importante notar que a comunicação é baseada em fluxos de bytes. Não há delimitação automática de mensagens; o script e o coprocesso devem concordar sobre o formato (por exemplo, linhas terminadas por \n). No exemplo acima, usamos linhas, mas em outros casos pode ser necessário usar protocolos mais complexos, como JSON ou campos de comprimento fixo.

Casos de uso

Coprocessos são úteis em situações onde você precisa de interação contínua com um programa. Alguns casos comuns incluem:

  • Calculadoras e interpretadores interativos: como bc, python, node, etc., onde você pode enviar expressões e obter resultados sem iniciar o processo a cada vez.
  • Servidores locais: se você tiver um servidor que aceita comandos via stdin/stdout, pode usar um coprocesso para manter uma conexão persistente.
  • Filtros com estado: programas que mantêm estado entre chamadas, como um contador ou um acumulador, podem ser executados como coprocessos para evitar reinicialização.
  • Automação de ferramentas interativas: ferramentas como ftp, telnet, ou ssh podem ser controladas via coprocessos, embora seja recomendável usar expect para casos complexos.

Vamos a um exemplo prático: usar um coprocesso com python3 para realizar cálculos científicos que exigem estado, como uma série de operações em uma matriz. Mas para simplificar, vamos implementar um contador em Python:

#!/bin/bash

# Coprocesso que mantém um contador
coproc COUNTER { python3 -c "
count = 0
while True:
    line = sys.stdin.readline()
    if not line:
        break
    if line.strip() == 'inc':
        count += 1
        print(count, flush=True)
    elif line.strip() == 'reset':
        count = 0
        print(0, flush=True)
    else:
        print('Comando desconhecido', flush=True)
"; }

# Incrementa algumas vezes
for i in 1 2 3; do
    echo "inc" >&${COUNTER[1]}
    read -u ${COUNTER[0]} valor
    echo "Contador: $valor"
done

# Reseta
echo "reset" >&${COUNTER[1]}
read -u ${COUNTER[0]} valor
echo "Após reset: $valor"

# Encerra
echo "" >&${COUNTER[1]}
wait $COUNTER_PID

Neste exemplo, o coprocesso mantém um contador em memória e responde a comandos inc e reset. Isso demonstra como evitar o custo de iniciar um novo processo a cada interação.

Limitações

Apesar de poderosos, os coprocessos têm limitações que você deve conhecer:

  • Suporte a apenas um coprocesso por vez: O Bash só permite um coprocesso ativo por vez. Se você tentar iniciar outro coproc enquanto um já está em execução, o Bash emitirá um erro e o novo coprocesso não será iniciado. Isso limita a complexidade de scripts que precisam de múltiplas comunicações simultâneas.
  • Sem sincronização automática: O script e o coprocesso são processos separados, e a comunicação é assíncrona. Se o script tentar ler uma resposta antes de enviar um comando, pode ficar bloqueado. É necessário gerenciar o fluxo de dados cuidadosamente para evitar deadlocks.
  • Buffer de saída: Muitos programas bufferizam sua saída quando não estão conectados a um terminal. Isso pode causar atrasos na comunicação. É recomendável usar opções como stdbuf ou chamar flush no programa para forçar a saída imediata.
  • Mensagens delimitadas: Como a comunicação é baseada em fluxo, não há delimitação automática de mensagens. O script deve definir um protocolo, como linhas ou tamanho fixo, e garantir que o coprocesso o siga.
  • Encerramento: Para encerrar um coprocesso, você precisa enviar um sinal ou fechar o descritor de escrita. Fechar o descritor de escrita fará com que o coprocesso receba EOF e possa terminar. Depois, use wait para aguardar o processo terminar.

Vamos testar a limitação de um único coprocesso:

#!/bin/bash

coproc A { cat; }
# Tentativa de iniciar outro coprocesso
coproc B { cat; } 2>&1
# O erro será exibido, e B não será iniciado
echo "B está definido? ${B[@]:-não}"

Ao executar, você verá uma mensagem de erro como bash: coproc: B: coproc: cannot use: only one coproc allowed. Isso mostra que apenas um coprocesso pode existir por vez.

Boas práticas

  • Use nomes significativos: Dê nomes descritivos aos coprocessos para facilitar a leitura do código.
  • Feche os descritores quando não precisar mais: Se você não vai enviar mais dados, feche o descritor de escrita com exec {FD}>&- para que o coprocesso receba EOF.
  • Implemente timeouts: Para evitar deadlocks, use read -t para ler com timeout e tratar a ausência de resposta.
  • Use ferramentas como expect para interações complexas: Se você precisa de controle de terminal ou sincronização avançada, considere usar expect em vez de coprocessos.

Exercícios

  1. Escreva um script que use um coprocesso com bc para calcular a soma de dois números fornecidos pelo usuário. O script deve ler dois números, enviar a expressão para o coprocesso e exibir o resultado.

    ✓ Resposta:
    #!/bin/bash
    
    coproc BC { bc; }
    
    read -p "Digite o primeiro número: " a
    read -p "Digite o segundo número: " b
    
    echo "$a + $b" >&${BC[1]}
    read -u ${BC[0]} resultado
    
    echo "A soma é: $resultado"
    
    echo "quit" >&${BC[1]}
    wait $BC_PID
  2. Crie um coprocesso que execute python3 e responda com o quadrado de um número enviado. O script deve enviar vários números e imprimir os resultados.

    ✓ Resposta:
    #!/bin/bash
    
    coproc PY { python3 -c "
    import sys
    for line in sys.stdin:
        n = int(line.strip())
        print(n*n, flush=True)
    "; }
    
    for num in 1 2 3 4 5; do
        echo "$num" >&${PY[1]}
        read -u ${PY[0]} quadrado
        echo "$num^2 = $quadrado"
    done
    
    echo "" >&${PY[1]}
    wait $PY_PID
  3. Explique o que acontece se você tentar iniciar um segundo coprocesso enquanto outro está ativo. Ilustre com um exemplo.

    ✓ Resposta: O Bash exibirá um erro indicando que apenas um coprocesso é permitido. O novo coprocesso não será iniciado. Exemplo:
    #!/bin/bash
    
    coproc A { cat; }
    # Tentativa de iniciar outro coprocesso
    coproc B { cat; } 2>&1
    # O erro será exibido, e B não será iniciado
    echo "B está definido? ${B[@]:-não}"
  4. Escreva um script que use um coprocesso para manter uma lista de itens em memória (por exemplo, em Python) e permita adicionar e listar itens. O script deve aceitar comandos do usuário: add <item> e list.

    ✓ Resposta:
    #!/bin/bash
    
    coproc LIST { python3 -c "
    import sys
    items = []
    for line in sys.stdin:
        line = line.strip()
        if line.startswith('add '):
            items.append(line[4:])
            print('Item adicionado', flush=True)
        elif line == 'list':
            for item in items:
                print(item, flush=True)
        else:
            print('Comando desconhecido', flush=True)
    "; }
    
    while true; do
        read -p "> " cmd
        if [[ $cmd == "sair" ]]; then
            echo "" >&${LIST[1]}
            break
        fi
        echo "$cmd" >&${LIST[1]}
        while read -u ${LIST[0]} resp; do
            if [[ $cmd == list* ]]; then
                # Para list, continuamos lendo até linha vazia
                if [[ -z $resp ]]; then
                    break
                fi
            else
                break
            fi
            echo "$resp"
        done
    done
    
    wait $LIST_PID
  5. Discuta as limitações dos coprocessos e sugira uma alternativa para cenários mais complexos.

    ✓ Resposta: As limitações incluem: apenas um coprocesso por vez, falta de sincronização, problemas de buffer, e comunicação baseada em fluxo sem delimitação automática. Para cenários complexos, alternativas incluem usar FIFOs (named pipes) para criar múltiplos canais de comunicação, usar expect para interações com programas que exigem terminal, ou usar sockets com ferramentas como socat ou netcat. Em aplicações mais robustas, considerar usar uma linguagem de script mais poderosa como Python ou Perl, que oferecem melhores abstrações para comunicação entre processos.

Referências