Por que existe uma condição de corrida
Os dois lados de um tubo são executados em paralelo, não um após o outro. Existe uma maneira muito simples de demonstrar isso: execute
time sleep 1 | sleep 1
Isso leva um segundo, não dois.
O shell inicia dois processos filhos e aguarda a conclusão de ambos. Esses dois processos são executados em paralelo: a única razão pela qual um deles seria sincronizado com o outro é quando ele precisa esperar pelo outro. O ponto mais comum de sincronização é quando o lado direito bloqueia a espera de leitura dos dados em sua entrada padrão e fica desbloqueado quando o lado esquerdo grava mais dados. O inverso também pode acontecer, quando o lado direito é lento para ler dados e o lado esquerdo bloqueia em sua operação de gravação até que o lado direito leia mais dados (há um buffer no próprio tubo, gerenciado pelo kernel, mas tem um tamanho máximo pequeno).
Para observar um ponto de sincronização, observe os seguintes comandos ( sh -x
imprime cada comando à medida que o executa):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
Brinque com variações até se sentir confortável com o que observa.
Dado o comando composto
cat tmp | head -1 > tmp
o processo do lado esquerdo faz o seguinte (listei apenas as etapas relevantes à minha explicação):
- Execute o programa externo
cat
com o argumento tmp
.
- Aberto
tmp
para leitura.
- Enquanto não chegou ao final do arquivo, leia um pedaço do arquivo e grave-o na saída padrão.
O processo do lado direito faz o seguinte:
- Redirecione a saída padrão para
tmp
, truncando o arquivo no processo.
- Execute o programa externo
head
com o argumento -1
.
- Leia uma linha da entrada padrão e grave-a na saída padrão.
O único ponto de sincronização é que o right-3 espera que o left-3 processe uma linha completa. Não há sincronização entre esquerda-2 e direita-1, para que elas possam ocorrer em qualquer ordem. Em que ordem elas acontecem não é previsível: depende da arquitetura da CPU, do shell, do kernel, de quais núcleos os processos estão agendados, do que interrompe a CPU que recebe nessa época, etc.
Como mudar o comportamento
Você não pode alterar o comportamento alterando uma configuração do sistema. O computador faz o que você pede. Você disse para truncar tmp
e ler tmp
em paralelo, o que faz as duas coisas em paralelo.
Ok, há uma “configuração do sistema” que você pode alterar: você pode substituir /bin/bash
por um programa diferente que não seja do bash. Espero que seja desnecessário dizer que essa não é uma boa ideia.
Se você deseja que o truncamento ocorra antes do lado esquerdo do tubo, é necessário colocá-lo fora do pipeline, por exemplo:
{ cat tmp | head -1; } >tmp
ou
( exec >tmp; cat tmp | head -1 )
Eu não tenho idéia do por que você iria querer isso. Qual é o sentido de ler um arquivo que você sabe que está vazio?
Por outro lado, se você deseja que o redirecionamento de saída (incluindo o truncamento) ocorra após a cat
conclusão da leitura, será necessário armazenar em buffer os dados na memória, por exemplo
line=$(cat tmp | head -1)
printf %s "$line" >tmp
ou escreva para um arquivo diferente e mova-o para o lugar. Geralmente, essa é a maneira robusta de fazer as coisas em scripts e tem a vantagem de o arquivo ser escrito por inteiro antes de ser visível pelo nome original.
cat tmp | head -1 >new && mv new tmp
A coleção moreutils inclui um programa que faz exatamente isso, chamado sponge
.
cat tmp | head -1 | sponge tmp
Como detectar o problema automaticamente
Se seu objetivo era pegar scripts mal escritos e descobrir automaticamente onde eles quebram, desculpe, a vida não é tão simples. A análise de tempo de execução não encontrará o problema com segurança, porque às vezes cat
termina a leitura antes que o truncamento aconteça. A análise estática pode, em princípio, fazê-lo; o exemplo simplificado da sua pergunta é capturado pelo Shellcheck , mas pode não encontrar um problema semelhante em um script mais complexo.