head come caracteres extras

15

Esperava-se que o comando shell a seguir 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 gera 1234512345 conforme o esperado. Mas isso funciona apenas na implementação coreutils do utilitário head . A implementação do busybox ainda come caracteres extras, então a saída é apenas 12345 .

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

O mesmo não pode ser dito para o caso em que a opção --bytes é 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 de corelibs usa essa oportunidade, mas o busybox não, ele ainda lê mais bytes do que o necessário em um buffer. É provavelmente feito para simplificar a implementação.

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

PS

Você precisa pressionar Ctrl+C para parar os comandos acima. Os utilitários Unix não falham na leitura além de EOF . Se você não quiser pressionar, você 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 eu não usei para simplificar.

    
por anton_rh 07.12.2017 / 13:28

3 respostas

30

Is it correct for the head utility to consume more characters from the input stream than it was asked?

Sim, é permitido (veja abaixo).

Is there some kind of standard for Unix utilities?

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

And if there is, does it specify this behavior?

Na sua introdução:

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.

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

O GNU head faz tentar deixar o descritor de arquivo na posição correta, mas é impossível procurar em canais, portanto, em seu teste, ele 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 read retorna 17 bytes (toda a entrada disponível), head processa quatro desses e depois tenta voltar 13 bytes, mas não consegue. (Você também pode ver aqui que o GNU head usa um buffer de 8 KiB.)

Quando você diz head para contar bytes (que não é padrão), ele sabe quantos bytes ler, então pode (se implementado dessa forma) limitar sua leitura de acordo. É por isso que seu teste head -c 5 funciona: o GNU head só lê 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, verá 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
    
por 07.12.2017 / 13:53
6

do POSIX

The head utility shall copy its input files to the standard output, ending the output for each file at a designated point.

Ele não diz nada sobre quanto head deve ler da entrada. Exigir que ele leia byte a byte seria tolo, já que seria extremamente lento na maioria dos casos.

Isso é, no entanto, tratado no read builtin / utility: todos os shells que eu posso encontrar read dos 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:

The read utility shall read a single logical line from standard input into one or more shell variables.

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

read someline
if something ; then 
    someprogram ...
fi

Aqui, a entrada padrão de someprogram é a mesma que a do shell, mas pode-se esperar que someprogram leia tudo o que vem depois da primeira linha de entrada consumida pelo read e não o que for foi deixado após uma leitura em buffer por read . Por outro lado, usar head como em seu exemplo é muito mais incomum.

Se você realmente quiser excluir todas as outras linhas, seria melhor (e mais rápido) usar alguma ferramenta que possa manipular 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'
    
por 07.12.2017 / 13:49
1
awk '{if (NR%2) == 1) print;}'
    
por 08.12.2017 / 17:14