A substituição de processo é um recurso poderoso do Bash que permite passar a saída de um comando como se fosse um arquivo para outro comando. Isso é especialmente útil quando você precisa comparar saídas de dois comandos ou usar a saída de um comando como entrada para outro sem criar arquivos temporários. Compreender esse mecanismo amplia sua capacidade de escrever scripts eficientes e elegantes.

Diferente de um pipe simples, que conecta a saída de um comando à entrada de outro, a substituição de processo cria um descritor de arquivo virtual que pode ser referenciado como um caminho de arquivo. Isso permite que você utilize comandos que esperam argumentos de arquivo, como diff ou comm, diretamente com a saída de outros comandos.

<(comando)

A sintaxe fundamental da substituição de processo é <(comando). O Bash executa o comando dentro dos parênteses e substitui a expressão por um caminho de arquivo (como /dev/fd/63) que, quando lido, produz a saída do comando. Esse recurso é parte da especificação POSIX e está disponível em shells como Bash, Zsh e Ksh.

Por exemplo, imagine que você queira listar os arquivos de dois diretórios e compará-los com diff. Sem substituição de processo, você precisaria criar arquivos temporários:

ls dir1 > /tmp/lista1.txt
ls dir2 > /tmp/lista2.txt
diff /tmp/lista1.txt /tmp/lista2.txt
rm /tmp/lista1.txt /tmp/lista2.txt

Com substituição de processo, você pode fazer isso em uma única linha, sem arquivos temporários:

diff <(ls dir1) <(ls dir2)

O Bash expande <(ls dir1) para algo como /dev/fd/63, que é um pipe ligado à saída do comando ls dir1. O comando diff então lê desse descritor como se fosse um arquivo.

Comparando saídas

Um dos usos mais comuns da substituição de processo é comparar a saída de dois comandos. Ferramentas como diff, comm e cmp esperam argumentos de arquivo, e a substituição de processo permite fornecer a saída de comandos diretamente. Isso é especialmente útil para comparar listas de arquivos, conteúdo de variáveis ou resultados de consultas.

Por exemplo, para comparar as variáveis de ambiente de dois processos, você pode usar:

diff <(env | sort) <(env -i bash -c 'env' | sort)

Outro exemplo: comparar a lista de pacotes instalados em dois sistemas (se você tiver acesso a eles) ou comparar a saída de dois comandos que geram listas ordenadas. O comando comm é particularmente útil para encontrar linhas em comum ou exclusivas entre dois arquivos ordenados:

comm -3 <(sort arquivo1.txt) <(sort arquivo2.txt)

A substituição de processo também pode ser usada com diff para comparar a saída de um comando com um arquivo real, como em diff arquivo.txt <(comando).

Casos de uso

Além de comparações, a substituição de processo é útil em vários cenários:

  • Processar saídas múltiplas: Você pode usar a substituição de processo para passar a saída de vários comandos para um único comando que aceita múltiplos arquivos, como diff ou paste.
  • Evitar arquivos temporários: Em scripts, reduz a necessidade de criar e limpar arquivos temporários, tornando o código mais limpo e menos propenso a erros.
  • Loop sobre saídas: Embora seja possível usar while read com um pipe, a substituição de processo pode ser mais conveniente quando você precisa preservar variáveis dentro de um loop (já que um pipe cria um subshell). Por exemplo:
while IFS= read -r linha; do
  echo "Processando: $linha"
done < <(comando_que_gera_saida)

Nesse caso, o loop é executado no shell atual, permitindo que variáveis modificadas dentro do loop persistam após o término.

  • Testar saídas como argumentos: Alguns comandos, como grep, aceitam múltiplos arquivos; você pode usar a substituição de processo para incluir a saída de um comando como um dos arquivos.

Exemplos

Vamos explorar alguns exemplos práticos para consolidar o entendimento.

Exemplo 1: Comparar a lista de arquivos de dois diretórios

diff <(ls -1 dir1) <(ls -1 dir2)

Isso mostra as diferenças entre os nomes dos arquivos nos dois diretórios. A opção -1 do ls garante uma saída em uma coluna, facilitando a comparação linha a linha.

Exemplo 2: Usar comm para encontrar linhas comuns entre dois arquivos ordenados

comm -12 <(sort lista1.txt) <(sort lista2.txt)

O comando comm -12 suprime as colunas 1 e 2, mostrando apenas as linhas comuns aos dois arquivos. Aqui, a substituição de processo permite ordenar os arquivos na hora, sem criar arquivos temporários ordenados.

Exemplo 3: Processar a saída de um comando em um loop sem subshell

total=0
while IFS= read -r num; do
  total=$((total + num))
done < <(seq 1 10)
echo "Soma de 1 a 10: $total"

Se você usasse um pipe (seq 1 10 | while ...), a variável total seria alterada em um subshell e a alteração seria perdida. Com a substituição de processo, o loop é executado no shell atual, e a variável total é atualizada corretamente.

Exemplo 4: Passar a saída de um comando como argumento para grep em múltiplos arquivos

grep -n "erro" <(dmesg) /var/log/syslog

Aqui, dmesg é executado e sua saída é tratada como um arquivo, permitindo que o grep pesquise a palavra "erro" tanto na saída de dmesg quanto no arquivo /var/log/syslog.

Boas práticas e observações finais

Ao usar a substituição de processo, lembre-se de que o comando dentro de <( ) é executado em um subshell, mas sua saída é disponibilizada como um arquivo. Isso pode ter implicações de desempenho se o comando for muito pesado, pois a saída pode ser armazenada em buffer. Em geral, é seguro e eficiente para a maioria dos casos.

Outra observação: a substituição de processo não está disponível em todos os shells. Se você escrever scripts para portabilidade, verifique se o shell alvo suporta esse recurso (Bash, Zsh e Ksh suportam).

Por fim, sempre que possível, prefira a substituição de processo a criar arquivos temporários, pois isso torna o script mais limpo e evita problemas com limpeza em caso de falhas.

Referências

Exercícios

  1. Escreva um comando que use substituição de processo para comparar a saída de ls -1 /etc com a saída de ls -1 /usr usando diff.
  2. ✓ Resposta:
    diff <(ls -1 /etc) <(ls -1 /usr)
  3. Explique por que o loop a seguir não atualiza a variável count fora do loop, e como você corrigiria usando substituição de processo.
  4. count=0
    seq 1 5 | while read n; do
      count=$((count + n))
    done
    echo "Soma: $count"

    ✓ Resposta: O pipe cria um subshell, e as variáveis alteradas dentro do subshell não são propagadas para o shell pai. Para corrigir, use a substituição de processo:
    count=0
    while read n; do
      count=$((count + n))
    done < <(seq 1 5)
    echo "Soma: $count"
  5. Use a substituição de processo com o comando comm para exibir apenas as linhas exclusivas do primeiro arquivo (não presentes no segundo). Considere que os arquivos a.txt e b.txt já estão ordenados.
  6. ✓ Resposta:
    comm -23 <(cat a.txt) <(cat b.txt)
    O -23 suprime as colunas 2 e 3, deixando apenas as linhas exclusivas do primeiro arquivo.
  7. Escreva um comando que use substituição de processo para mostrar as linhas comuns entre a saída de ps -e e o conteúdo de um arquivo processos.txt (que contém nomes de processos, um por linha). Dica: use grep -f.
  8. ✓ Resposta:
    grep -f processos.txt <(ps -e)
    Isso fará com que o grep use os padrões do arquivo para buscar na saída de ps -e.
  9. Crie um script que use substituição de processo para comparar a saída de date com a saída de date -u e exiba uma mensagem indicando se são iguais ou diferentes. Use diff e verifique o código de saída.
  10. ✓ Resposta:
    #!/bin/bash
    if diff <(date) <(date -u) > /dev/null; then
      echo "As saídas são iguais."
    else
      echo "As saídas são diferentes."
    fi