Note que o problema não é com tail
, mas com head
aqui, que lê a partir do pipe mais do que a primeira linha a ser produzida (então não sobra nada para tail
).
E sim, é compatível com POSIX.
head
é necessário para deixar o cursor dentro de stdin logo após a última linha que ele gerou quando a entrada é procurada, mas não o contrário.
link :
When a standard utility reads a seekable input file and terminates without an error before it reaches end-of-file, the utility shall ensure that the file offset in the open file description is properly positioned just past the last byte processed by the utility. For files that are not seekable, the state of the file offset in the open file description for that file is unspecified.
Por head
poder fazer isso para um arquivo não-pesquisável, isso significaria que ele teria que ler um byte de cada vez, o que seria terrivelmente ineficiente¹. Isso é o que o utilitário read
ou line
faz ou o GNU sed
com a opção -u
.
Você pode substituir head -n 20
por gsed -u 20q
se quiser esse comportamento.
Embora aqui você prefira querer:
sed -e 1b -e '$b' -e d
em vez disso. Aqui, apenas uma chamada de ferramenta, portanto, nenhum problema com um buffer interno que não pode ser compartilhado entre duas chamadas de ferramenta. Note, entretanto, que para arquivos grandes, ele será menos eficiente, pois sed
lê todo o arquivo, enquanto que para arquivos pesquisáveis tail
pularia a maior parte dele procurando próximo ao final do arquivo.
Veja a discussão relacionada sobre buffering em Por que usar um loop de shell para processar texto é considerado uma prática ruim? .
Observe que tail
deve gerar a saída do fluxo no stdin. Embora, como uma otimização e para arquivos pesquisáveis, as implementações possam buscar o final do arquivo para obter os dados finais de lá, não é permitido retornar a um ponto que seria antes da posição inicial no momento tail
foi invocado (Busybox tail
costumava ter esse bug).
Então, por exemplo, em:
{ cat; tail -n 1; } < file
Mesmo que tail
possa voltar para a última linha de file
, isso não acontece. Seu stdin é um fluxo vazio, pois cat
deixou o cursor no final do arquivo; não é permitido recuperar dados desse fluxo procurando mais para trás no arquivo.
(Texto acima riscado pendente de esclarecimento pelo Grupo Aberto e considerando que não é feito corretamente por várias implementações)
¹ O head
embutido de ksh93
(habilitado se você colocar /opt/ast/bin
à frente de $PATH
), para sockets (um tipo de arquivos não-pesquisáveis) em vez disso espreita na entrada (usando recvfrom(..., MSG_PEEK)
) antes de realmente ler para ver quanto precisa ler para se certificar de que não lê muito. E volta a ler um byte de cada vez para outros tipos de arquivos. Isso é um pouco mais eficiente e acredito que seja a principal razão pela qual ele implementa seus pipes com socketpair()
s em vez de pipe()
. Note que não é completamente à prova de erros pois existe uma condição de corrida que pode ser desencadeada se outro processo for lido a partir do socket entre o peek e o read .