cabeça come caracteres extras


15

Esperava-se que o seguinte comando shell imprimisse apenas linhas ímpares do fluxo de entrada:

echo -e "aaa\nbbb\nccc\nddd\n" | (while true; do head -n 1; head -n 1 >/dev/null; done)

Mas em vez disso, apenas imprime a primeira linha: aaa.

O mesmo não acontece quando usado com a opção -c( --bytes):

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 >/dev/null; done)

Este comando é exibido 1234512345conforme o esperado. Mas isso funciona apenas na implementação de coreutils do headutilitário. A implementação do busybox ainda consome caracteres extras, portanto a saída é justa 12345.

Eu acho que essa maneira específica de implementação é feita para fins de otimização. Você não pode saber onde a linha termina, portanto, não sabe quantos caracteres precisa ler. A única maneira de não consumir caracteres extras do fluxo de entrada é ler o fluxo byte a byte. Mas a leitura do fluxo, um byte de cada vez, pode ser lenta. Então, eu acho que headlê o fluxo de entrada em um buffer grande o suficiente e depois conta as linhas nesse buffer.

O mesmo não pode ser dito para o caso em que a --bytesopção é usada. Nesse caso, você sabe quantos bytes precisa ler. Então você pode ler exatamente esse número de bytes e não mais do que isso. A implementação do corelibs usa essa oportunidade, mas a do busybox não, ela ainda lê mais bytes do que o necessário em um buffer. Provavelmente isso é feito para simplificar a implementação.

Então a pergunta. É correto que o headutilitário consuma mais caracteres do fluxo de entrada do que o solicitado? Existe algum tipo de padrão para utilitários Unix? E se houver, ele especifica esse comportamento?

PS

Você precisa pressionar Ctrl+Cpara interromper os comandos acima. Os utilitários Unix não falham na leitura além EOF. Se você não quiser pressionar, pode usar um comando mais complexo:

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 | [ `wc -c` -eq 0 ] && break >/dev/null; done)

que não usei por simplicidade.


2
Neardupe unix.stackexchange.com/questions/48777/… e unix.stackexchange.com/questions/84011/… . Além disso, se este título tinha sido em movies.SX minha resposta seria Zardoz :)
dave_thompson_085

Respostas:


30

É correto que o utilitário principal consuma mais caracteres do fluxo de entrada do que o solicitado?

Sim, é permitido (veja abaixo).

Existe algum tipo de padrão para utilitários Unix?

Sim, POSIX volume 3, Shell e Utilitários .

E se houver, ele especifica esse comportamento?

Faz, em sua introdução:

Quando um utilitário padrão lê um arquivo de entrada procurável e termina sem erro antes de chegar ao final do arquivo, o utilitário deve garantir que o deslocamento do arquivo na descrição do arquivo aberto seja posicionado corretamente logo após o último byte processado pelo utilitário. Para arquivos que não podem ser procurados, o estado do deslocamento do arquivo na descrição do arquivo aberto para esse arquivo não é especificado.

headé um dos utilitários padrão , portanto, uma implementação em conformidade com POSIX deve implementar o comportamento descrito acima.

GNU head não tentar deixar o descritor de arquivo na posição correta, mas é impossível para buscar em tubos, por isso, o teste não consegue restaurar a posição. Você pode ver isso usando strace:

$ echo -e "aaa\nbbb\nccc\nddd\n" | strace head -n 1
...
read(0, "aaa\nbbb\nccc\nddd\n\n", 8192) = 17
lseek(0, -13, SEEK_CUR)                 = -1 ESPIPE (Illegal seek)
...

O readretorna 17 bytes (toda a entrada disponível), headprocessa quatro deles e tenta retornar 13 bytes, mas não pode. (Você também pode ver aqui que o GNU headusa um buffer de 8 KiB.)

Quando você diz headpara contar bytes (o que não é padrão), ele sabe quantos bytes ler, para que possa (se implementado dessa maneira) limitar sua leitura adequadamente. É por isso que seu head -c 5teste funciona: o GNU headlê apenas cinco bytes e, portanto, não precisa restaurar a posição do descritor de arquivo.

Se você gravar o documento em um arquivo e usá-lo, obterá o comportamento que deseja:

$ echo -e "aaa\nbbb\nccc\nddd\n" > file
$ < file (while true; do head -n 1; head -n 1 >/dev/null; done)
aaa
ccc

2
Pode-se usar os utilitários line(agora removidos do POSIX / XPG, mas ainda disponíveis em muitos sistemas) ou read( IFS= read -r line) que lêem um byte por vez para evitar o problema.
Stéphane Chazelas

3
Observe que se head -c 5a leitura de 5 bytes ou um buffer completo depende da implementação (observe também que isso head -cnão é padrão), você não pode confiar nisso. Você precisaria dd bs=1 count=5ter uma garantia de que não mais que 5 bytes serão lidos.
Stéphane Chazelas

Obrigado @ Stéphane, atualizei a -c 5descrição.
Stephen Kitt

Observe que o headbuilt-in ksh93lê um byte de cada vez, head -n 1quando a entrada não é procurável.
Stéphane Chazelas

1
@anton_rh, ddsó funciona corretamente com pipes, bs=1se você usar um countcomo leituras em pipes pode retornar menos do que o solicitado (mas pelo menos um byte, a menos que eof seja alcançado). O GNU ddtem iflag=fullblockque pode aliviar isso.
Stéphane Chazelas

6

POSIX

O utilitário principal deve copiar seus arquivos de entrada na saída padrão, finalizando a saída de cada arquivo em um ponto designado.

Não diz nada sobre o quanto head deve ler a partir da entrada. Exigir que ele leia byte a byte seria tolo, pois seria extremamente lento na maioria dos casos.

No entanto, isso é abordado no readutilitário / embutido: todas as conchas que posso encontrar readdos pipes um byte de cada vez e o texto padrão pode ser interpretado como significando que isso deve ser feito, para poder ler apenas uma única linha:

O utilitário de leitura deve ler uma única linha lógica da entrada padrão em uma ou mais variáveis ​​de shell.

No caso de read, que é usado em scripts de shell, um caso de uso comum seria algo como isto:

read someline
if something ; then 
    someprogram ...
fi

Aqui, a entrada padrão de someprogramé a mesma do shell, mas pode-se esperar que someprogramconsiga ler tudo o que vem após a primeira linha de entrada consumida pelo reade não o que resta depois de uma leitura em buffer read. Por outro lado, usar headcomo no seu exemplo é muito mais incomum.


Se você realmente deseja excluir todas as outras linhas, seria melhor (e mais rápido) usar alguma ferramenta que possa lidar com toda a entrada de uma só vez, por exemplo

$ seq 1 10 | sed -ne '1~2p'   # GNU sed
$ seq 1 10 | sed -e 'n;d'     # works in GNU sed and the BSD sed on macOS

$ seq 1 10 | awk 'NR % 2' 
$ seq 1 10 | perl -ne 'print if $. % 2'

Mas veja a seção “INPUT FILES” da introdução do POSIX ao volume 3 ...
Stephen Kitt

1
O POSIX diz: "Quando um utilitário padrão lê um arquivo de entrada procurável e termina sem erro antes de chegar ao final do arquivo, o utilitário deve garantir que o deslocamento do arquivo na descrição do arquivo aberto seja posicionado adequadamente após o último byte processado pelo . o utilitário para arquivos que não são pesquisável, o estado do arquivo compensado na descrição do arquivo aberto para esse arquivo não é especificado. "
AlexP

2
Observe que, a menos que você use -r, readpode ler mais de uma linha (sem IFS=ela também removeria espaços e guias à esquerda e à direita (com o valor padrão de $IFS)).
Stéphane Chazelas

@ AlexP, sim, Stephen acabou de vincular essa parte.
Ilkkachu

Observe que o headbuilt-in ksh93lê um byte de cada vez, head -n 1quando a entrada não é procurável.
Stéphane Chazelas

1
awk '{if (NR%2) == 1) print;}'

Hellóka :-) e bem-vindo ao site! Observe que preferimos as respostas mais elaboradas. Eles devem ser úteis para os googlers do futuro.
peterh - Restabelece Monica
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.